@stigmer/react 3.12.5 → 3.12.6

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 (208) hide show
  1. package/agent/AgentDetailView.js +2 -2
  2. package/agent/AgentDetailView.js.map +1 -1
  3. package/agent/index.d.ts +0 -1
  4. package/agent/index.d.ts.map +1 -1
  5. package/agent/index.js +0 -1
  6. package/agent/index.js.map +1 -1
  7. package/agent/useUpdateAgent.d.ts +3 -3
  8. package/agent/useUpdateAgent.js +3 -3
  9. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  10. package/agent-instance/AgentInstanceDetailPanel.js +5 -7
  11. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  12. package/agent-instance/useUpdateAgentInstance.d.ts +1 -1
  13. package/agent-instance/useUpdateAgentInstance.js +1 -1
  14. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  15. package/channel/AgentChannelsPanel.js +10 -5
  16. package/channel/AgentChannelsPanel.js.map +1 -1
  17. package/channel/ChannelRunConfigDialog.d.ts +51 -0
  18. package/channel/ChannelRunConfigDialog.d.ts.map +1 -0
  19. package/channel/ChannelRunConfigDialog.js +121 -0
  20. package/channel/ChannelRunConfigDialog.js.map +1 -0
  21. package/channel/index.d.ts +2 -0
  22. package/channel/index.d.ts.map +1 -1
  23. package/channel/index.js +1 -0
  24. package/channel/index.js.map +1 -1
  25. package/channel/useSaveAgentChannel.d.ts.map +1 -1
  26. package/channel/useSaveAgentChannel.js +6 -3
  27. package/channel/useSaveAgentChannel.js.map +1 -1
  28. package/channel-app/ChannelAppDetailPanel.js +9 -9
  29. package/channel-app/ChannelAppDetailPanel.js.map +1 -1
  30. package/composer/ComposerToolbar.d.ts +6 -1
  31. package/composer/ComposerToolbar.d.ts.map +1 -1
  32. package/composer/ComposerToolbar.js +2 -2
  33. package/composer/ComposerToolbar.js.map +1 -1
  34. package/composer/SessionComposer.d.ts +9 -0
  35. package/composer/SessionComposer.d.ts.map +1 -1
  36. package/composer/SessionComposer.js +22 -3
  37. package/composer/SessionComposer.js.map +1 -1
  38. package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
  39. package/cursor-accounts/CursorAccountsConsole.js +12 -3
  40. package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
  41. package/cursor-accounts/cursor-account-coverage.d.ts +30 -1
  42. package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -1
  43. package/cursor-accounts/cursor-account-coverage.js +21 -0
  44. package/cursor-accounts/cursor-account-coverage.js.map +1 -1
  45. package/execution/useCreateAgentExecution.d.ts +15 -0
  46. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  47. package/execution/useCreateAgentExecution.js +5 -0
  48. package/execution/useCreateAgentExecution.js.map +1 -1
  49. package/identity-account/AccountPreferencesPanel.d.ts +2 -1
  50. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  51. package/identity-account/AccountPreferencesPanel.js +33 -18
  52. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  53. package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
  54. package/identity-provider/IdentityProviderDetailPanel.js +16 -15
  55. package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
  56. package/index.d.ts +4 -4
  57. package/index.d.ts.map +1 -1
  58. package/index.js +3 -3
  59. package/index.js.map +1 -1
  60. package/mcp-server/McpServerDetailView.js +2 -2
  61. package/mcp-server/McpServerDetailView.js.map +1 -1
  62. package/mcp-server/index.d.ts +0 -1
  63. package/mcp-server/index.d.ts.map +1 -1
  64. package/mcp-server/index.js +0 -1
  65. package/mcp-server/index.js.map +1 -1
  66. package/mcp-server/internal/definitionDrift.d.ts +14 -12
  67. package/mcp-server/internal/definitionDrift.d.ts.map +1 -1
  68. package/mcp-server/internal/definitionDrift.js +22 -19
  69. package/mcp-server/internal/definitionDrift.js.map +1 -1
  70. package/mcp-server/useUpdateMcpServer.d.ts +3 -3
  71. package/mcp-server/useUpdateMcpServer.js +3 -3
  72. package/models/ModelSelector.d.ts +33 -1
  73. package/models/ModelSelector.d.ts.map +1 -1
  74. package/models/ModelSelector.js +37 -4
  75. package/models/ModelSelector.js.map +1 -1
  76. package/models/index.d.ts +2 -0
  77. package/models/index.d.ts.map +1 -1
  78. package/models/index.js +1 -0
  79. package/models/index.js.map +1 -1
  80. package/models/registry.d.ts +16 -0
  81. package/models/registry.d.ts.map +1 -1
  82. package/models/registry.js +13 -0
  83. package/models/registry.js.map +1 -1
  84. package/models/thinking-mode.d.ts +27 -0
  85. package/models/thinking-mode.d.ts.map +1 -0
  86. package/models/thinking-mode.js +32 -0
  87. package/models/thinking-mode.js.map +1 -0
  88. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
  89. package/oauth-app/OAuthAppDetailPanel.js +21 -20
  90. package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
  91. package/package.json +4 -4
  92. package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -1
  93. package/platform-client/PlatformClientDetailPanel.js +12 -13
  94. package/platform-client/PlatformClientDetailPanel.js.map +1 -1
  95. package/schedule/ScheduleDetailView.d.ts.map +1 -1
  96. package/schedule/ScheduleDetailView.js +21 -10
  97. package/schedule/ScheduleDetailView.js.map +1 -1
  98. package/schedule/ScheduleForm.d.ts.map +1 -1
  99. package/schedule/ScheduleForm.js +13 -3
  100. package/schedule/ScheduleForm.js.map +1 -1
  101. package/session/run-config.d.ts +9 -0
  102. package/session/run-config.d.ts.map +1 -1
  103. package/session/run-config.js +5 -0
  104. package/session/run-config.js.map +1 -1
  105. package/session/useNewSessionFlow.d.ts.map +1 -1
  106. package/session/useNewSessionFlow.js +6 -0
  107. package/session/useNewSessionFlow.js.map +1 -1
  108. package/session/useSessionConversation.d.ts +12 -1
  109. package/session/useSessionConversation.d.ts.map +1 -1
  110. package/session/useSessionConversation.js +15 -24
  111. package/session/useSessionConversation.js.map +1 -1
  112. package/session/useSessionPageFlow.d.ts.map +1 -1
  113. package/session/useSessionPageFlow.js +14 -10
  114. package/session/useSessionPageFlow.js.map +1 -1
  115. package/session/useUpdateSession.d.ts +18 -24
  116. package/session/useUpdateSession.d.ts.map +1 -1
  117. package/session/useUpdateSession.js +18 -24
  118. package/session/useUpdateSession.js.map +1 -1
  119. package/src/agent/AgentDetailView.tsx +2 -2
  120. package/src/agent/index.ts +0 -2
  121. package/src/agent/useUpdateAgent.ts +3 -3
  122. package/src/agent-instance/AgentInstanceDetailPanel.tsx +5 -7
  123. package/src/agent-instance/__tests__/AgentInstanceDetailPanel.test.tsx +75 -0
  124. package/src/agent-instance/useUpdateAgentInstance.ts +1 -1
  125. package/src/channel/AgentChannelsPanel.tsx +26 -1
  126. package/src/channel/ChannelRunConfigDialog.tsx +351 -0
  127. package/src/channel/__tests__/ChannelRunConfigDialog.test.tsx +210 -0
  128. package/src/channel/index.ts +2 -0
  129. package/src/channel/useSaveAgentChannel.ts +6 -3
  130. package/src/channel-app/ChannelAppDetailPanel.tsx +9 -9
  131. package/src/channel-app/__tests__/channelAppWhatsApp.test.tsx +2 -0
  132. package/src/channel-app/__tests__/channelAppWipeSafety.test.tsx +87 -0
  133. package/src/composer/ComposerToolbar.tsx +9 -0
  134. package/src/composer/SessionComposer.tsx +36 -2
  135. package/src/composer/__tests__/SessionComposer-thinkingMode.test.tsx +176 -0
  136. package/src/cursor-accounts/CursorAccountsConsole.tsx +40 -5
  137. package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +32 -0
  138. package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +58 -1
  139. package/src/cursor-accounts/cursor-account-coverage.ts +55 -0
  140. package/src/execution/useCreateAgentExecution.ts +19 -0
  141. package/src/identity-account/AccountPreferencesPanel.tsx +154 -68
  142. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +36 -2
  143. package/src/identity-provider/IdentityProviderDetailPanel.tsx +18 -15
  144. package/src/identity-provider/__tests__/IdentityProviderDetailPanel.test.tsx +80 -0
  145. package/src/index.ts +2 -2
  146. package/src/mcp-server/McpServerDetailView.tsx +2 -2
  147. package/src/mcp-server/index.ts +0 -2
  148. package/src/mcp-server/internal/definitionDrift.ts +22 -20
  149. package/src/mcp-server/useUpdateMcpServer.ts +3 -3
  150. package/src/models/ModelSelector.tsx +94 -1
  151. package/src/models/index.ts +2 -0
  152. package/src/models/registry.ts +29 -0
  153. package/src/models/thinking-mode.ts +48 -0
  154. package/src/oauth-app/OAuthAppDetailPanel.tsx +23 -21
  155. package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +94 -0
  156. package/src/platform-client/PlatformClientDetailPanel.tsx +14 -13
  157. package/src/platform-client/__tests__/PlatformClientDetailPanel.test.tsx +82 -0
  158. package/src/schedule/ScheduleDetailView.tsx +29 -4
  159. package/src/schedule/ScheduleForm.tsx +14 -1
  160. package/src/session/run-config.ts +16 -0
  161. package/src/session/useNewSessionFlow.ts +6 -0
  162. package/src/session/useSessionConversation.ts +32 -40
  163. package/src/session/useSessionPageFlow.ts +14 -11
  164. package/src/session/useUpdateSession.ts +18 -24
  165. package/src/workflow/ApprovalFormBuilder.tsx +20 -5
  166. package/src/workflow/WorkflowDetailView.tsx +2 -2
  167. package/src/workflow/__tests__/WorkflowInstanceDetailPanel.test.tsx +83 -0
  168. package/src/workflow/index.ts +0 -2
  169. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +6 -7
  170. package/src/workflow/useUpdateWorkflow.ts +3 -3
  171. package/styles.css +1 -1
  172. package/workflow/ApprovalFormBuilder.d.ts.map +1 -1
  173. package/workflow/ApprovalFormBuilder.js +10 -8
  174. package/workflow/ApprovalFormBuilder.js.map +1 -1
  175. package/workflow/WorkflowDetailView.js +2 -2
  176. package/workflow/WorkflowDetailView.js.map +1 -1
  177. package/workflow/index.d.ts +0 -1
  178. package/workflow/index.d.ts.map +1 -1
  179. package/workflow/index.js +0 -1
  180. package/workflow/index.js.map +1 -1
  181. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  182. package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -7
  183. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  184. package/workflow/useUpdateWorkflow.d.ts +3 -3
  185. package/workflow/useUpdateWorkflow.js +3 -3
  186. package/agent/internal/agentToInput.d.ts +0 -15
  187. package/agent/internal/agentToInput.d.ts.map +0 -1
  188. package/agent/internal/agentToInput.js +0 -84
  189. package/agent/internal/agentToInput.js.map +0 -1
  190. package/mcp-server/internal/mcpServerToInput.d.ts +0 -12
  191. package/mcp-server/internal/mcpServerToInput.d.ts.map +0 -1
  192. package/mcp-server/internal/mcpServerToInput.js +0 -84
  193. package/mcp-server/internal/mcpServerToInput.js.map +0 -1
  194. package/session/session-spec-converters.d.ts +0 -24
  195. package/session/session-spec-converters.d.ts.map +0 -1
  196. package/session/session-spec-converters.js +0 -72
  197. package/session/session-spec-converters.js.map +0 -1
  198. package/src/agent/internal/__tests__/agentToInput.test.ts +0 -145
  199. package/src/agent/internal/agentToInput.ts +0 -102
  200. package/src/mcp-server/__tests__/mcpServerToInput.test.ts +0 -201
  201. package/src/mcp-server/internal/mcpServerToInput.ts +0 -98
  202. package/src/session/__tests__/session-spec-converters.test.ts +0 -183
  203. package/src/session/session-spec-converters.ts +0 -86
  204. package/src/workflow/internal/workflowToInput.ts +0 -96
  205. package/workflow/internal/workflowToInput.d.ts +0 -12
  206. package/workflow/internal/workflowToInput.d.ts.map +0 -1
  207. package/workflow/internal/workflowToInput.js +0 -73
  208. package/workflow/internal/workflowToInput.js.map +0 -1
@@ -1,6 +1,14 @@
1
1
  "use client";
2
2
 
3
- import { type FormEvent, useCallback, useEffect, useId, useMemo, useState } from "react";
3
+ import {
4
+ type FormEvent,
5
+ type ReactNode,
6
+ useCallback,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useState,
11
+ } from "react";
4
12
  import { cn } from "@stigmer/theme";
5
13
  import { getUserMessage, toIdentityAccountUpdateInput } from "@stigmer/sdk";
6
14
  import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
@@ -23,7 +31,8 @@ export interface AccountPreferencesPanelProps {
23
31
 
24
32
  /**
25
33
  * Self-contained editor for the current user's declared preferences
26
- * (`IdentityAccountSpec.preferences.standing_context`).
34
+ * (`IdentityAccountSpec.preferences`): standing context plus the
35
+ * execution defaults (default harness and per-harness default models).
27
36
  *
28
37
  * The declared text is snapshotted into the user's own eligible agent
29
38
  * executions and delivered to the agent as background context.
@@ -267,31 +276,60 @@ function AccountPreferencesForm({
267
276
  </p>
268
277
  </div>
269
278
 
270
- <DefaultHarnessRadioGroup
271
- baseId={baseId}
272
- value={defaultHarness}
273
- onChange={setDefaultHarness}
274
- disabled={isUpdating}
275
- />
276
-
277
- <div className="stg:grid stg:gap-3 stg:sm:grid-cols-2">
278
- <DefaultModelSelect
279
- id={`${baseId}-default-native-model`}
280
- harness="native"
281
- label={`Default model — ${HARNESS_META.native.label}`}
282
- value={defaultNativeModel}
283
- onChange={setDefaultNativeModel}
279
+ <fieldset className="stg:space-y-2" disabled={isUpdating}>
280
+ <legend className="stg:text-xs stg:font-medium stg:text-foreground stg:mb-1">
281
+ Default harness
282
+ </legend>
283
+ <HarnessOptionRow
284
+ radioName={`${baseId}-default-harness`}
285
+ value=""
286
+ label="Platform default"
287
+ description="Stigmer picks the harness for new sessions."
288
+ checked={defaultHarness === ""}
289
+ onSelect={setDefaultHarness}
284
290
  disabled={isUpdating}
285
291
  />
286
- <DefaultModelSelect
287
- id={`${baseId}-default-cursor-model`}
288
- harness="cursor"
289
- label={`Default model — ${HARNESS_META.cursor.label}`}
290
- value={defaultCursorModel}
291
- onChange={setDefaultCursorModel}
292
+ <HarnessOptionRow
293
+ radioName={`${baseId}-default-harness`}
294
+ value="native"
295
+ label={HARNESS_META.native.label}
296
+ description={HARNESS_META.native.description}
297
+ checked={defaultHarness === "native"}
298
+ onSelect={setDefaultHarness}
292
299
  disabled={isUpdating}
293
- />
294
- </div>
300
+ >
301
+ <DefaultModelSelect
302
+ id={`${baseId}-default-native-model`}
303
+ harness="native"
304
+ label="Default model"
305
+ value={defaultNativeModel}
306
+ onChange={setDefaultNativeModel}
307
+ disabled={isUpdating}
308
+ />
309
+ </HarnessOptionRow>
310
+ <HarnessOptionRow
311
+ radioName={`${baseId}-default-harness`}
312
+ value="cursor"
313
+ label={HARNESS_META.cursor.label}
314
+ description={HARNESS_META.cursor.description}
315
+ checked={defaultHarness === "cursor"}
316
+ onSelect={setDefaultHarness}
317
+ disabled={isUpdating}
318
+ >
319
+ <DefaultModelSelect
320
+ id={`${baseId}-default-cursor-model`}
321
+ harness="cursor"
322
+ label="Default model"
323
+ value={defaultCursorModel}
324
+ onChange={setDefaultCursorModel}
325
+ disabled={isUpdating}
326
+ />
327
+ </HarnessOptionRow>
328
+ <p className="stg:text-[0.65rem] stg:text-muted-foreground">
329
+ Each harness&apos;s default model applies whenever a session runs
330
+ on that harness — even when it isn&apos;t your default.
331
+ </p>
332
+ </fieldset>
295
333
  </div>
296
334
 
297
335
  {updateError && (
@@ -332,60 +370,104 @@ function AccountPreferencesForm({
332
370
  }
333
371
 
334
372
  /**
335
- * Only harnesses with a shipped runtime are offerable as a default — must
336
- * stay in lockstep with the proto's `default_harness` in-list validation.
337
- */
338
- const OFFERABLE_HARNESSES: readonly HarnessOption[] = ["native", "cursor"];
339
-
340
- /**
341
- * Default-harness choice: Platform default plus each shipped harness.
342
- * A mutually exclusive, always-visible set radio group, not a select.
373
+ * One selectable default-harness choice, presented as a bordered row
374
+ * (the `VisibilityOptionRow` presentation shape): radio + label +
375
+ * one-line description, with the harness's own controls nested below.
376
+ *
377
+ * The rows for shipped harnesses embed that harness's default-model
378
+ * select as `children` — the pairing IS the information architecture:
379
+ * a harness and its model default belong to each other, and each model
380
+ * default applies whenever a session runs on that harness, independent
381
+ * of which harness is the default. The non-selected harness's select
382
+ * therefore stays interactive (visually secondary, never disabled).
383
+ *
384
+ * The nested controls live OUTSIDE the radio's `<label>` on purpose:
385
+ * a `<select>` inside the label would toggle the radio on every
386
+ * interaction with it.
387
+ *
388
+ * The shipped-harness rows must stay in lockstep with the proto's
389
+ * `default_harness` in-list validation (`["native", "cursor"]`) — each
390
+ * shipped harness binds to its own proto model field, so a new harness
391
+ * adds a field, a row, and a validation entry together.
343
392
  */
344
- function DefaultHarnessRadioGroup({
345
- baseId,
393
+ function HarnessOptionRow({
394
+ radioName,
346
395
  value,
347
- onChange,
396
+ label,
397
+ description,
398
+ checked,
399
+ onSelect,
348
400
  disabled,
401
+ children,
349
402
  }: {
350
- readonly baseId: string;
403
+ readonly radioName: string;
351
404
  readonly value: string;
352
- readonly onChange: (value: string) => void;
405
+ readonly label: string;
406
+ readonly description: string;
407
+ readonly checked: boolean;
408
+ readonly onSelect: (value: string) => void;
353
409
  readonly disabled: boolean;
410
+ readonly children?: ReactNode;
354
411
  }) {
355
- const name = `${baseId}-default-harness`;
356
- const options: readonly { value: string; label: string }[] = [
357
- { value: "", label: "Platform default" },
358
- ...OFFERABLE_HARNESSES.map((h) => ({ value: h, label: HARNESS_META[h].label })),
359
- ];
412
+ // The radio's accessible name is the title alone (aria-labelledby), with
413
+ // the description attached as aria-describedby the wrapping label keeps
414
+ // the whole text block clickable without bloating the announced name.
415
+ const titleId = `${radioName}-${value || "platform"}-title`;
416
+ const descriptionId = `${radioName}-${value || "platform"}-description`;
360
417
 
361
418
  return (
362
- <fieldset className="stg:space-y-1" disabled={disabled}>
363
- <legend className="stg:text-xs stg:font-medium stg:text-foreground">
364
- Default harness
365
- </legend>
366
- <div className="stg:flex stg:flex-wrap stg:gap-x-4 stg:gap-y-1">
367
- {options.map((option) => (
368
- <label
369
- key={option.value}
370
- className={cn(
371
- "stg:inline-flex stg:cursor-pointer stg:items-center stg:gap-1.5 stg:text-xs stg:text-foreground",
372
- disabled && "stg:cursor-default stg:opacity-50",
373
- )}
419
+ <div
420
+ className={cn(
421
+ "stg:rounded-md stg:border stg:px-3 stg:py-2.5 stg:transition-colors",
422
+ checked
423
+ ? "stg:border-primary stg:bg-accent"
424
+ : "stg:border-input stg:hover:bg-accent-hover",
425
+ disabled && "stg:opacity-50",
426
+ )}
427
+ >
428
+ <label
429
+ className={cn(
430
+ "stg:flex stg:cursor-pointer stg:items-start stg:gap-2",
431
+ disabled && "stg:cursor-default",
432
+ )}
433
+ >
434
+ <input
435
+ type="radio"
436
+ name={radioName}
437
+ value={value}
438
+ checked={checked}
439
+ onChange={() => onSelect(value)}
440
+ disabled={disabled}
441
+ aria-labelledby={titleId}
442
+ aria-describedby={descriptionId}
443
+ className="stg:mt-0.5 stg:accent-primary stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring"
444
+ />
445
+ <span className="stg:min-w-0 stg:flex-1">
446
+ <span
447
+ id={titleId}
448
+ className="stg:block stg:text-xs stg:font-medium stg:text-foreground"
374
449
  >
375
- <input
376
- type="radio"
377
- name={name}
378
- value={option.value}
379
- checked={value === option.value}
380
- onChange={() => onChange(option.value)}
381
- disabled={disabled}
382
- className="stg:accent-primary stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring"
383
- />
384
- {option.label}
385
- </label>
386
- ))}
387
- </div>
388
- </fieldset>
450
+ {label}
451
+ </span>
452
+ <span
453
+ id={descriptionId}
454
+ className="stg:block stg:text-[0.65rem] stg:leading-snug stg:text-muted-foreground"
455
+ >
456
+ {description}
457
+ </span>
458
+ </span>
459
+ </label>
460
+ {children && (
461
+ <div
462
+ className={cn(
463
+ "stg:mt-2 stg:pl-6 stg:transition-opacity",
464
+ !checked && "stg:opacity-70",
465
+ )}
466
+ >
467
+ {children}
468
+ </div>
469
+ )}
470
+ </div>
389
471
  );
390
472
  }
391
473
 
@@ -426,6 +508,10 @@ function DefaultModelSelect({
426
508
  id={id}
427
509
  value={value}
428
510
  onChange={(e) => onChange(e.target.value)}
511
+ // Both harness rows show the same visible label; the accessible
512
+ // name stays unique per control by qualifying it with the harness
513
+ // (WCAG label-in-name: the visible text remains a prefix).
514
+ aria-label={`${label} — ${HARNESS_META[harness].label}`}
429
515
  disabled={disabled || isLoading}
430
516
  className={cn(
431
517
  "stg:w-full stg:rounded-md stg:border stg:border-input stg:bg-background stg:px-2.5 stg:py-1.5 stg:text-xs stg:text-foreground",
@@ -107,7 +107,10 @@ describe("AccountPreferencesPanel", () => {
107
107
  // Fields this form never renders must round-trip untouched.
108
108
  expect(input.idpId).toBe("auth0|abc");
109
109
  expect(input.email).toBe("ada@acme.example");
110
- // The update pipeline addresses the resource by org + slug.
110
+ // The update pipeline addresses id-first — the id is REQUIRED here:
111
+ // identity accounts are org-less, so the org+slug fallback can never
112
+ // match one (the save-path bug this pin guards against).
113
+ expect(input.id).toBe("ia-1");
111
114
  expect(input.org).toBe("acme");
112
115
  expect(input.slug).toBe("ada");
113
116
  });
@@ -149,7 +152,7 @@ describe("AccountPreferencesPanel", () => {
149
152
  },
150
153
  });
151
154
 
152
- it("renders the harness radio group and registry-fed model selects", async () => {
155
+ it("renders the harness option rows and registry-fed model selects", async () => {
153
156
  renderPanel(createMockStigmer());
154
157
  await findSyncedField("Keep answers terse.");
155
158
 
@@ -160,6 +163,37 @@ describe("AccountPreferencesPanel", () => {
160
163
  expect(screen.getByLabelText("Default model — Cursor")).toBeTruthy();
161
164
  });
162
165
 
166
+ it("pairs each harness row with its description (the row IS the context)", async () => {
167
+ renderPanel(createMockStigmer());
168
+ await findSyncedField("Keep answers terse.");
169
+
170
+ // Row subtitles come from HARNESS_META — the single source of harness
171
+ // display copy — plus the platform-default explainer.
172
+ expect(screen.getByText("Stigmer picks the harness for new sessions.")).toBeTruthy();
173
+ expect(screen.getByText("Stigmer's native agent runtime")).toBeTruthy();
174
+ expect(screen.getByText("Cursor IDE agent with codebase indexing")).toBeTruthy();
175
+ // The radios' accessible descriptions are wired via aria-describedby.
176
+ expect(
177
+ screen.getByRole("radio", { name: "Stigmer" }).getAttribute("aria-describedby"),
178
+ ).toBeTruthy();
179
+ });
180
+
181
+ it("keeps the non-selected harness's model select editable (per-harness models are independent of the default harness)", async () => {
182
+ renderPanel(createMockStigmer({ whoAmI: vi.fn(async () => ACCOUNT_WITH_DEFAULTS) }));
183
+ await findSyncedField("Keep answers terse.");
184
+
185
+ // Default harness is cursor, yet the Stigmer model stays settable:
186
+ // the launcher applies the model of the session's ACTIVE harness, so
187
+ // hiding or disabling it would remove a real capability.
188
+ await waitFor(() => {
189
+ expect(screen.getByRole("radio", { name: "Cursor" })).toHaveProperty("checked", true);
190
+ });
191
+ const nativeSelect = screen.getByLabelText("Default model — Stigmer") as HTMLSelectElement;
192
+ expect(nativeSelect.disabled).toBe(false);
193
+ fireEvent.change(nativeSelect, { target: { value: "claude-sonnet-4.6" } });
194
+ expect(nativeSelect.value).toBe("claude-sonnet-4.6");
195
+ });
196
+
163
197
  it("reflects the saved defaults in the form", async () => {
164
198
  renderPanel(createMockStigmer({ whoAmI: vi.fn(async () => ACCOUNT_WITH_DEFAULTS) }));
165
199
  await findSyncedField("Keep answers terse.");
@@ -3,7 +3,7 @@
3
3
  import { type FormEvent, useCallback, useId, useRef, useState } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { UNSTYLED_FIELDSET } from "../internal/element-resets.js";
6
- import { getUserMessage } from "@stigmer/sdk";
6
+ import { getUserMessage, toIdentityProviderUpdateInput } from "@stigmer/sdk";
7
7
  import type { IdentityProvider } from "@stigmer/protos/ai/stigmer/iam/identityprovider/v1/api_pb";
8
8
  import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
9
9
  import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";
@@ -132,10 +132,13 @@ export function IdentityProviderDetailPanel({
132
132
  e.preventDefault();
133
133
  clearError();
134
134
  try {
135
+ // Full-spec-replace safety: spread the complete mapped input and
136
+ // override only the edited fields, so unlisted spec fields (e.g.
137
+ // rate_limit_budget) survive the save. Fields the form clears
138
+ // (SSO providers have no JIT settings) are set to undefined
139
+ // explicitly — omitting them would carry the stale mapped value.
135
140
  const updated = await update({
136
- name: meta?.name ?? "",
137
- slug: meta?.slug,
138
- org: meta?.org ?? "",
141
+ ...toIdentityProviderUpdateInput(identityProvider),
139
142
  displayName: displayName.trim(),
140
143
  jwksUri: jwksUri.trim(),
141
144
  allowedIssuers: issuers
@@ -146,16 +149,16 @@ export function IdentityProviderDetailPanel({
146
149
  userinfoEndpoint: userinfoEndpoint.trim() || undefined,
147
150
  isSsoProvider: isSso,
148
151
  oidcClientId: isSso ? oidcClientId.trim() : undefined,
149
- ...(!isSso && {
150
- autoProvisionAccounts: autoProvision,
151
- autoGrantOnOrg: autoGrant,
152
- ...(autoGrant && autoGrantRole !== IamRole.iam_role_unspecified && {
153
- autoGrantRole,
154
- }),
155
- ...(autoGrant && tenantOrgClaim.trim() && {
156
- tenantOrgClaim: tenantOrgClaim.trim(),
157
- }),
158
- }),
152
+ autoProvisionAccounts: !isSso && autoProvision ? true : undefined,
153
+ autoGrantOnOrg: !isSso && autoGrant ? true : undefined,
154
+ autoGrantRole:
155
+ !isSso && autoGrant && autoGrantRole !== IamRole.iam_role_unspecified
156
+ ? autoGrantRole
157
+ : undefined,
158
+ tenantOrgClaim:
159
+ !isSso && autoGrant && tenantOrgClaim.trim()
160
+ ? tenantOrgClaim.trim()
161
+ : undefined,
159
162
  });
160
163
  setMode("view");
161
164
  onUpdated?.(updated);
@@ -164,7 +167,7 @@ export function IdentityProviderDetailPanel({
164
167
  }
165
168
  },
166
169
  [
167
- meta, displayName, jwksUri, issuers, audience,
170
+ identityProvider, displayName, jwksUri, issuers, audience,
168
171
  userinfoEndpoint, isSso, oidcClientId, autoProvision, autoGrant,
169
172
  autoGrantRole, tenantOrgClaim, update, clearError, onUpdated,
170
173
  ],
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ IdentityProviderSchema,
6
+ type IdentityProvider,
7
+ } from "@stigmer/protos/ai/stigmer/iam/identityprovider/v1/api_pb";
8
+ import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
9
+ import type { IdentityProviderInput } from "@stigmer/sdk";
10
+ import { StigmerContext } from "../../context";
11
+ import { IdentityProviderDetailPanel } from "../IdentityProviderDetailPanel";
12
+
13
+ /**
14
+ * Regression suite for the full-spec-replace wipe bug: before the
15
+ * generated-mapper migration, this panel hand-built its update input and
16
+ * NEVER sent `rate_limit_budget` — editing any IdP field zeroed the
17
+ * provider's rate limit. The panel must spread
18
+ * `toIdentityProviderUpdateInput` and override only what it edits.
19
+ */
20
+
21
+ const IDP: IdentityProvider = create(IdentityProviderSchema, {
22
+ metadata: {
23
+ id: "idp-1",
24
+ name: "Acme Okta",
25
+ slug: "acme-okta",
26
+ org: "acme",
27
+ },
28
+ spec: {
29
+ displayName: "Acme Okta",
30
+ jwksUri: "https://acme.okta.example/jwks",
31
+ allowedIssuers: ["https://acme.okta.example"],
32
+ expectedAudience: "stigmer",
33
+ rateLimitBudget: 120,
34
+ isSsoProvider: false,
35
+ autoProvisionAccounts: true,
36
+ autoGrantOnOrg: true,
37
+ autoGrantRole: IamRole.admin,
38
+ tenantOrgClaim: "org_slug",
39
+ },
40
+ });
41
+
42
+ function renderPanel(update: ReturnType<typeof vi.fn>) {
43
+ const client = {
44
+ identityProvider: { update },
45
+ } as never;
46
+ return render(
47
+ <StigmerContext.Provider value={client}>
48
+ <IdentityProviderDetailPanel identityProvider={IDP} />
49
+ </StigmerContext.Provider>,
50
+ );
51
+ }
52
+
53
+ afterEach(cleanup);
54
+
55
+ describe("IdentityProviderDetailPanel save payload", () => {
56
+ it("round-trips rate_limit_budget on a display-name edit (the wipe bug)", async () => {
57
+ const update = vi.fn(async (_input: IdentityProviderInput) => IDP);
58
+ renderPanel(update);
59
+
60
+ fireEvent.click(screen.getByRole("button", { name: "Edit" }));
61
+ const displayName = await screen.findByLabelText("Display name");
62
+ fireEvent.change(displayName, { target: { value: "Acme Okta (renamed)" } });
63
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
64
+
65
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
66
+ const input = update.mock.calls[0]![0];
67
+
68
+ // The wipe-bug guard: the form does not render the rate limit, yet it
69
+ // must survive the save.
70
+ expect(input.rateLimitBudget).toBe(120);
71
+ // The edited field.
72
+ expect(input.displayName).toBe("Acme Okta (renamed)");
73
+ // JIT settings round-trip from the edit state.
74
+ expect(input.autoGrantRole).toBe(IamRole.admin);
75
+ expect(input.tenantOrgClaim).toBe("org_slug");
76
+ // Addressing fields.
77
+ expect(input.org).toBe("acme");
78
+ expect(input.slug).toBe("acme-okta");
79
+ });
80
+ });
package/src/index.ts CHANGED
@@ -567,7 +567,6 @@ export {
567
567
  McpToolSelector,
568
568
  useCreateMcpServer,
569
569
  useUpdateMcpServer,
570
- mcpServerToInput,
571
570
  toServerKey,
572
571
  IdentityTransportStep,
573
572
  EnvironmentAuthStep,
@@ -696,7 +695,6 @@ export {
696
695
  useDefaultAgent,
697
696
  useCreateAgent,
698
697
  useUpdateAgent,
699
- agentToInput,
700
698
  AgentCreationWizard,
701
699
  } from "./agent/index.js";
702
700
  export type {
@@ -1311,6 +1309,7 @@ export {
1311
1309
  ConnectSlackDialog,
1312
1310
  ConnectWhatsAppDialog,
1313
1311
  ChannelCredentialsDialog,
1312
+ ChannelRunConfigDialog,
1314
1313
  ChannelToolCredentials,
1315
1314
  ChannelConversationsDialog,
1316
1315
  ChannelTemplatesDialog,
@@ -1334,6 +1333,7 @@ export type {
1334
1333
  ConnectSlackDialogProps,
1335
1334
  ConnectWhatsAppDialogProps,
1336
1335
  ChannelCredentialsDialogProps,
1336
+ ChannelRunConfigDialogProps,
1337
1337
  ChannelToolCredentialsProps,
1338
1338
  ChannelConversationsDialogProps,
1339
1339
  ChannelTemplatesDialogProps,
@@ -16,7 +16,7 @@ import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/envir
16
16
  import { useMcpServer } from "./useMcpServer.js";
17
17
  import type { UseMcpServerReturn } from "./useMcpServer.js";
18
18
  import { useUpdateMcpServer } from "./useUpdateMcpServer.js";
19
- import { mcpServerToInput } from "./internal/mcpServerToInput.js";
19
+ import { toMcpServerUpdateInput } from "@stigmer/sdk";
20
20
  import { useMcpServerConnect } from "./useMcpServerConnect.js";
21
21
  import { useMcpServerCredentials } from "./useMcpServerCredentials.js";
22
22
  import {
@@ -214,7 +214,7 @@ export function McpServerDetailView({
214
214
  value: import("@stigmer/sdk").McpServerInput[K],
215
215
  ): Promise<boolean> => {
216
216
  if (!mcpServer) return false;
217
- const input = mcpServerToInput(mcpServer);
217
+ const input = toMcpServerUpdateInput(mcpServer);
218
218
  (input as unknown as Record<string, unknown>)[field] = value;
219
219
  try {
220
220
  const updated = await updateMcpServer(input);
@@ -95,8 +95,6 @@ export type { UseCreateMcpServerReturn } from "./useCreateMcpServer.js";
95
95
  export { useUpdateMcpServer } from "./useUpdateMcpServer.js";
96
96
  export type { UseUpdateMcpServerReturn } from "./useUpdateMcpServer.js";
97
97
 
98
- export { mcpServerToInput } from "./internal/mcpServerToInput.js";
99
-
100
98
  export { McpServerCreationWizard } from "./McpServerCreationWizard.js";
101
99
  export type {
102
100
  McpServerCreationWizardProps,
@@ -1,6 +1,5 @@
1
1
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
2
- import type { McpServerInput } from "@stigmer/sdk";
3
- import { mcpServerToInput } from "./mcpServerToInput.js";
2
+ import { toMcpServerUpdateInput, type McpServerInput } from "@stigmer/sdk";
4
3
 
5
4
  /**
6
5
  * Connection-defining field groups a marketplace definition can drift on.
@@ -24,9 +23,9 @@ export type DriftFieldId =
24
23
 
25
24
  /**
26
25
  * Deep equality over the plain-object projections produced by
27
- * `mcpServerToInput`. Key order is irrelevant (proto map iteration order
28
- * is not canonical); `undefined` values and absent keys are equivalent
29
- * (the conversion uses `undefined` for proto defaults).
26
+ * `toMcpServerUpdateInput`. Key order is irrelevant (proto map iteration
27
+ * order is not canonical); `undefined` values and absent keys are
28
+ * equivalent (the conversion uses `undefined` for proto defaults).
30
29
  */
31
30
  function deepEqual(a: unknown, b: unknown): boolean {
32
31
  if (Object.is(a, b)) return true;
@@ -72,17 +71,18 @@ function functionalEnvShape(env: McpServerInput["env"]) {
72
71
  * mirrors and reports which connection-defining field groups differ, or
73
72
  * `null` when the connection-defining configuration matches.
74
73
  *
75
- * The comparison runs on `mcpServerToInput` projections rather than raw
76
- * protos: that conversion is the SDK's canonical read-modify-write lens,
77
- * and its exhaustiveness is fence-tested so a new spec field cannot
78
- * silently bypass this comparison either.
74
+ * The comparison runs on `toMcpServerUpdateInput` projections rather than
75
+ * raw protos: that generated mapper is the SDK's canonical
76
+ * read-modify-write lens, and its completeness is guaranteed by codegen
77
+ * plus the SDK's schema-tripwire fixture suite — so a new spec field
78
+ * cannot silently bypass this comparison either.
79
79
  */
80
80
  export function computeDefinitionDrift(
81
81
  current: McpServer,
82
82
  template: McpServer,
83
83
  ): readonly DriftFieldId[] | null {
84
- const cur = mcpServerToInput(current);
85
- const tpl = mcpServerToInput(template);
84
+ const cur = toMcpServerUpdateInput(current);
85
+ const tpl = toMcpServerUpdateInput(template);
86
86
 
87
87
  const changed: DriftFieldId[] = [];
88
88
 
@@ -128,21 +128,22 @@ export function computeDefinitionDrift(
128
128
  *
129
129
  * Definition fields — transport, env declarations, auth, and the cosmetic
130
130
  * description/icon/tags/repository metadata — come from the template.
131
- * The user's own choices survive: identity (name/org/slug/labels), tool
132
- * enablement, and pinned approval policies. Visibility is not part of the
133
- * input contract at all (the backend preserves it on update;
134
- * `updateVisibility` is its dedicated write path).
131
+ * The user's own choices survive: identity (name/org/slug/labels/
132
+ * visibility the backend ignores visibility on update anyway;
133
+ * `updateVisibility` is its dedicated write path), tool enablement, and
134
+ * pinned approval policies.
135
135
  *
136
- * Built by merging two `mcpServerToInput` projections, so the preserved
137
- * set is the explicit override list below and nothing can be lost in
138
- * translation (the conversion is fence-tested exhaustive).
136
+ * Built by merging two `toMcpServerUpdateInput` projections, so the
137
+ * preserved set is the explicit override list below and nothing can be
138
+ * lost in translation (the mapper is generated complete and guarded by
139
+ * the SDK's schema-tripwire fixture suite).
139
140
  */
140
141
  export function buildRefreshInput(
141
142
  current: McpServer,
142
143
  template: McpServer,
143
144
  ): McpServerInput {
144
- const cur = mcpServerToInput(current);
145
- const tpl = mcpServerToInput(template);
145
+ const cur = toMcpServerUpdateInput(current);
146
+ const tpl = toMcpServerUpdateInput(template);
146
147
 
147
148
  return {
148
149
  ...tpl,
@@ -151,6 +152,7 @@ export function buildRefreshInput(
151
152
  org: cur.org,
152
153
  slug: cur.slug,
153
154
  labels: cur.labels,
155
+ visibility: cur.visibility,
154
156
  // User policy: never reset by a definition refresh.
155
157
  defaultEnabledTools: cur.defaultEnabledTools,
156
158
  pinnedToolApprovals: cur.pinnedToolApprovals,
@@ -23,9 +23,9 @@ export interface UseUpdateMcpServerReturn {
23
23
  *
24
24
  * Wraps `stigmer.mcpServer.update(input)` with loading and error state.
25
25
  * The caller must provide a **complete** `McpServerInput` — the backend
26
- * performs full spec replacement. Use `mcpServerToInput()` to reconstruct
27
- * the input from a fetched server, modify the desired field, and pass
28
- * the result here.
26
+ * performs full spec replacement. Use `toMcpServerUpdateInput()` (from
27
+ * `@stigmer/sdk`) to reconstruct the input from a fetched server, modify
28
+ * the desired field, and pass the result here.
29
29
  */
30
30
  export function useUpdateMcpServer(): UseUpdateMcpServerReturn {
31
31
  const stigmer = useStigmer();