@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
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
+ import type { AgentChannelInput } from "@stigmer/sdk";
6
+ import { StigmerContext } from "../../context";
7
+ import { ModelRegistryContext } from "../../models/ModelRegistryContext";
8
+ import type { ModelInfo } from "../../models/registry";
9
+ import { ChannelRunConfigDialog } from "../ChannelRunConfigDialog";
10
+
11
+ // happy-dom does not implement the native dialog show/close methods.
12
+ beforeAll(() => {
13
+ HTMLDialogElement.prototype.showModal = function showModal() {
14
+ this.open = true;
15
+ };
16
+ HTMLDialogElement.prototype.close = function close() {
17
+ this.open = false;
18
+ };
19
+ if (!("ResizeObserver" in globalThis)) {
20
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
21
+ class {
22
+ observe() {}
23
+ unobserve() {}
24
+ disconnect() {}
25
+ };
26
+ }
27
+ });
28
+
29
+ afterEach(cleanup);
30
+
31
+ /**
32
+ * The console's first run_config editor (stigmer/stigmer#792). The pinned
33
+ * behaviors are the edit-surface contracts the rest of the platform relies
34
+ * on: `max_tool_rounds` (the operator knob this editor never renders)
35
+ * survives every save verbatim, an all-empty draft clears the block
36
+ * ("empty = inherit"), and untouched variant switches stay off the wire
37
+ * (the #357/#772 unspecified-vs-explicit ledger distinction).
38
+ */
39
+
40
+ const MODELS: ModelInfo[] = [
41
+ {
42
+ modelId: "claude-haiku-4-5",
43
+ provider: "anthropic",
44
+ displayName: "Haiku 4.5",
45
+ shortDescription: "Fast Claude with optional thinking",
46
+ speedTier: "fast",
47
+ costTier: "economy",
48
+ harness: "cursor",
49
+ featured: true,
50
+ serviceTiers: [],
51
+ thinkingCapable: true,
52
+ },
53
+ ];
54
+
55
+ function makeChannel(runConfig?: {
56
+ modelName?: string;
57
+ maxCostUsd?: number;
58
+ maxToolRounds?: number;
59
+ serviceTier?: ServiceTier;
60
+ thinkingMode?: ThinkingMode;
61
+ }) {
62
+ return {
63
+ metadata: {
64
+ id: "ach_1",
65
+ name: "Support Slack",
66
+ slug: "support-slack",
67
+ org: "acme",
68
+ labels: {},
69
+ },
70
+ spec: {
71
+ agentRef: { org: "acme", slug: "support-agent" },
72
+ enabled: true,
73
+ providerConfig: { case: "slack", value: {} },
74
+ environmentRefs: [],
75
+ ...(runConfig
76
+ ? {
77
+ runConfig: {
78
+ modelName: runConfig.modelName ?? "",
79
+ maxCostUsd: runConfig.maxCostUsd ?? 0,
80
+ maxToolRounds: runConfig.maxToolRounds ?? 0,
81
+ serviceTier: runConfig.serviceTier ?? ServiceTier.UNSPECIFIED,
82
+ thinkingMode: runConfig.thinkingMode ?? ThinkingMode.UNSPECIFIED,
83
+ },
84
+ }
85
+ : {}),
86
+ },
87
+ status: { installState: 2 },
88
+ } as never;
89
+ }
90
+
91
+ function createMockStigmer() {
92
+ const apply = vi.fn().mockResolvedValue({});
93
+ const client = { agentChannel: { apply } } as never;
94
+ return { client, apply };
95
+ }
96
+
97
+ function Providers({
98
+ client,
99
+ children,
100
+ }: {
101
+ client: unknown;
102
+ children: ReactNode;
103
+ }) {
104
+ return (
105
+ <StigmerContext.Provider value={client as never}>
106
+ <ModelRegistryContext.Provider
107
+ value={{ models: MODELS, isLoading: false, error: null, refetch: vi.fn() }}
108
+ >
109
+ {children}
110
+ </ModelRegistryContext.Provider>
111
+ </StigmerContext.Provider>
112
+ );
113
+ }
114
+
115
+ function renderDialog(channel: unknown) {
116
+ const { client, apply } = createMockStigmer();
117
+ const onSaved = vi.fn();
118
+ render(
119
+ <Providers client={client}>
120
+ <ChannelRunConfigDialog
121
+ open
122
+ onOpenChange={vi.fn()}
123
+ channel={channel as never}
124
+ onSaved={onSaved}
125
+ />
126
+ </Providers>,
127
+ );
128
+ return { apply, onSaved };
129
+ }
130
+
131
+ async function save() {
132
+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
133
+ }
134
+
135
+ describe("ChannelRunConfigDialog", () => {
136
+ it("preserves max_tool_rounds verbatim even though it never renders", async () => {
137
+ const { apply } = renderDialog(
138
+ makeChannel({ modelName: "claude-haiku-4-5", maxToolRounds: 25 }),
139
+ );
140
+
141
+ expect(screen.queryByText(/tool round/i)).toBeNull();
142
+ await save();
143
+
144
+ await waitFor(() => expect(apply).toHaveBeenCalledOnce());
145
+ const input = apply.mock.calls[0][0] as AgentChannelInput;
146
+ expect(input.runConfig?.maxToolRounds).toBe(25);
147
+ expect(input.runConfig?.modelName).toBe("claude-haiku-4-5");
148
+ });
149
+
150
+ it("clears run_config entirely when the draft is emptied (empty = inherit)", async () => {
151
+ const { apply } = renderDialog(
152
+ makeChannel({ modelName: "claude-haiku-4-5", maxCostUsd: 2 }),
153
+ );
154
+
155
+ fireEvent.click(
156
+ screen.getByRole("button", { name: "Reset to platform default" }),
157
+ );
158
+ fireEvent.change(screen.getByLabelText(/Budget per run/), {
159
+ target: { value: "" },
160
+ });
161
+ await save();
162
+
163
+ await waitFor(() => expect(apply).toHaveBeenCalledOnce());
164
+ const input = apply.mock.calls[0][0] as AgentChannelInput;
165
+ expect(input.runConfig).toBeUndefined();
166
+ });
167
+
168
+ it("carries an actively enabled thinking mode, and only then", async () => {
169
+ const { apply } = renderDialog(makeChannel({ modelName: "claude-haiku-4-5" }));
170
+
171
+ fireEvent.click(screen.getByRole("button", { name: /Haiku 4\.5/ }));
172
+ fireEvent.click(await screen.findByRole("switch", { name: "Thinking" }));
173
+ fireEvent.keyDown(document.activeElement ?? document.body, { key: "Escape" });
174
+ await save();
175
+
176
+ await waitFor(() => expect(apply).toHaveBeenCalledOnce());
177
+ const input = apply.mock.calls[0][0] as AgentChannelInput;
178
+ expect(input.runConfig?.thinkingMode).toBe(ThinkingMode.ENABLED);
179
+ // The untouched tier stays absent — never stamped as an explicit
180
+ // STANDARD by a save (#357 ledger distinction).
181
+ expect(input.runConfig?.serviceTier).toBeUndefined();
182
+ });
183
+
184
+ it("round-trips a stored thinking pin into the switch state", async () => {
185
+ renderDialog(
186
+ makeChannel({
187
+ modelName: "claude-haiku-4-5",
188
+ thinkingMode: ThinkingMode.ENABLED,
189
+ }),
190
+ );
191
+
192
+ fireEvent.click(screen.getByRole("button", { name: /Haiku 4\.5/ }));
193
+ const toggle = await screen.findByRole("switch", { name: "Thinking" });
194
+ expect(toggle.getAttribute("aria-checked")).toBe("true");
195
+ });
196
+
197
+ it("collects a budget without requiring a model pin", async () => {
198
+ const { apply } = renderDialog(makeChannel());
199
+
200
+ fireEvent.change(screen.getByLabelText(/Budget per run/), {
201
+ target: { value: "1.5" },
202
+ });
203
+ await save();
204
+
205
+ await waitFor(() => expect(apply).toHaveBeenCalledOnce());
206
+ const input = apply.mock.calls[0][0] as AgentChannelInput;
207
+ expect(input.runConfig?.maxCostUsd).toBe(1.5);
208
+ expect(input.runConfig?.modelName).toBeUndefined();
209
+ });
210
+ });
@@ -69,6 +69,8 @@ export type { ConnectWhatsAppDialogProps } from "./ConnectWhatsAppDialog.js";
69
69
 
70
70
  export { ChannelCredentialsDialog } from "./ChannelCredentialsDialog.js";
71
71
  export type { ChannelCredentialsDialogProps } from "./ChannelCredentialsDialog.js";
72
+ export { ChannelRunConfigDialog } from "./ChannelRunConfigDialog.js";
73
+ export type { ChannelRunConfigDialogProps } from "./ChannelRunConfigDialog.js";
72
74
 
73
75
  export { ChannelToolCredentials } from "./ChannelToolCredentials.js";
74
76
  export type { ChannelToolCredentialsProps } from "./ChannelToolCredentials.js";
@@ -66,9 +66,11 @@ export function agentChannelToInput(channel: AgentChannel): AgentChannelInput {
66
66
  })),
67
67
  }
68
68
  : {}),
69
- // The console does not edit run_config (yet) it rides through
70
- // opaquely so a settings save or pause/resume toggle never wipes an
71
- // override set via the CLI or API.
69
+ // run_config rides through verbatim so a settings save or
70
+ // pause/resume toggle never wipes an override set elsewhere (the
71
+ // ChannelRunConfigDialog the one surface that edits it, #792 —
72
+ // overrides this copy with its own draft; CLI/API overrides survive
73
+ // every other save).
72
74
  ...(spec?.runConfig
73
75
  ? {
74
76
  runConfig: {
@@ -76,6 +78,7 @@ export function agentChannelToInput(channel: AgentChannel): AgentChannelInput {
76
78
  maxCostUsd: spec.runConfig.maxCostUsd,
77
79
  maxToolRounds: spec.runConfig.maxToolRounds,
78
80
  serviceTier: spec.runConfig.serviceTier,
81
+ thinkingMode: spec.runConfig.thinkingMode,
79
82
  },
80
83
  }
81
84
  : {}),
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { type FormEvent, useCallback, useId, useMemo, useState } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
- import { getUserMessage } from "@stigmer/sdk";
5
+ import { getUserMessage, toChannelAppUpdateInput } from "@stigmer/sdk";
6
6
  import type { ChannelApp } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
7
7
  import { useStigmer } from "../hooks.js";
8
8
  import { useUpdateChannelApp } from "./useUpdateChannelApp.js";
@@ -229,10 +229,10 @@ function SlackAppDetail({
229
229
 
230
230
  clearError();
231
231
  try {
232
+ // Full-spec-replace safety: spread the complete mapped input and
233
+ // override only the slack arm this form owns.
232
234
  const updated = await update({
233
- name,
234
- org: channelApp.metadata?.org ?? "",
235
- ...(channelApp.metadata?.slug ? { slug: channelApp.metadata.slug } : {}),
235
+ ...toChannelAppUpdateInput(channelApp),
236
236
  slack: {
237
237
  clientId: clientId.trim(),
238
238
  // The redaction marker means "keep the stored value" — the
@@ -246,7 +246,7 @@ function SlackAppDetail({
246
246
  // error state is managed by useUpdateChannelApp
247
247
  }
248
248
  },
249
- [canSave, clearError, update, name, channelApp.metadata, clientId, clientSecret, signingSecret, onUpdated],
249
+ [canSave, clearError, update, channelApp, clientId, clientSecret, signingSecret, onUpdated],
250
250
  );
251
251
 
252
252
  return (
@@ -379,10 +379,10 @@ function WhatsAppAppDetail({
379
379
 
380
380
  clearError();
381
381
  try {
382
+ // Full-spec-replace safety: spread the complete mapped input and
383
+ // override only the whatsapp arm this form owns.
382
384
  const updated = await update({
383
- name,
384
- org: channelApp.metadata?.org ?? "",
385
- ...(channelApp.metadata?.slug ? { slug: channelApp.metadata.slug } : {}),
385
+ ...toChannelAppUpdateInput(channelApp),
386
386
  whatsapp: {
387
387
  appId: metaAppId.trim(),
388
388
  // The redaction marker means "keep the stored value" — the
@@ -397,7 +397,7 @@ function WhatsAppAppDetail({
397
397
  // error state is managed by useUpdateChannelApp
398
398
  }
399
399
  },
400
- [canSave, clearError, update, name, channelApp.metadata, metaAppId, appSecret, accessToken, verifyToken, onUpdated],
400
+ [canSave, clearError, update, channelApp, metaAppId, appSecret, accessToken, verifyToken, onUpdated],
401
401
  );
402
402
 
403
403
  return (
@@ -228,6 +228,8 @@ describe("ChannelAppDetailPanel — WhatsApp branch", () => {
228
228
 
229
229
  await waitFor(() => expect(update).toHaveBeenCalled());
230
230
  expect(update).toHaveBeenCalledWith({
231
+ // The generated mapper carries metadata.id for exact update addressing.
232
+ id: "chapp_1",
231
233
  name: "Acme WhatsApp",
232
234
  org: "acme",
233
235
  slug: "acme-whatsapp",
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import {
5
+ ChannelAppSchema,
6
+ type ChannelApp,
7
+ } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
8
+ import type { ChannelAppInput } from "@stigmer/sdk";
9
+ import { StigmerContext } from "../../context";
10
+ import { ChannelAppDetailPanel } from "../ChannelAppDetailPanel";
11
+
12
+ /**
13
+ * Regression suite for the full-spec-replace wipe bug: both detail forms
14
+ * must spread `toChannelAppUpdateInput` and override only their provider
15
+ * arm, so identity fields and any future spec field survive the save.
16
+ * Secrets follow the redaction contract: the form is seeded with the
17
+ * server's redaction marker, and sending the marker back means "keep the
18
+ * stored value" (per-field, verified by the backend's
19
+ * TestRedactAndEncryptCoverEveryProviderArm).
20
+ */
21
+
22
+ const REDACTED = "***REDACTED***";
23
+
24
+ const SLACK_APP: ChannelApp = create(ChannelAppSchema, {
25
+ metadata: {
26
+ id: "chapp_1",
27
+ name: "Acme Slack",
28
+ slug: "acme-slack",
29
+ org: "acme",
30
+ labels: { team: "support" },
31
+ },
32
+ spec: {
33
+ providerConfig: {
34
+ case: "slack",
35
+ value: {
36
+ clientId: "1234.5678",
37
+ clientSecret: REDACTED,
38
+ signingSecret: REDACTED,
39
+ },
40
+ },
41
+ },
42
+ });
43
+
44
+ function renderPanel(update: ReturnType<typeof vi.fn>) {
45
+ const client = {
46
+ baseUrl: "https://api.stigmer.ai",
47
+ channelapp: { update, delete: vi.fn(async () => ({})) },
48
+ } as never;
49
+ return render(
50
+ <StigmerContext.Provider value={client}>
51
+ <ChannelAppDetailPanel
52
+ channelApp={SLACK_APP}
53
+ consoleOrigin="https://console.acme.example"
54
+ />
55
+ </StigmerContext.Provider>,
56
+ );
57
+ }
58
+
59
+ afterEach(cleanup);
60
+
61
+ describe("ChannelAppDetailPanel save payload (slack arm)", () => {
62
+ it("spreads the complete mapped input and overrides only the slack config", async () => {
63
+ const update = vi.fn(async (_input: ChannelAppInput) => SLACK_APP);
64
+ renderPanel(update);
65
+
66
+ const clientId = await screen.findByLabelText(/client id/i);
67
+ fireEvent.change(clientId, { target: { value: "9999.0000" } });
68
+ fireEvent.click(screen.getByRole("button", { name: /save credentials/i }));
69
+
70
+ await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
71
+ const input = update.mock.calls[0]![0];
72
+
73
+ // The edited field, plus per-field keep-markers for untouched secrets.
74
+ expect(input.slack).toEqual({
75
+ clientId: "9999.0000",
76
+ clientSecret: REDACTED,
77
+ signingSecret: REDACTED,
78
+ });
79
+ // Identity and unrendered metadata survive via the mapper spread.
80
+ expect(input.name).toBe("Acme Slack");
81
+ expect(input.org).toBe("acme");
82
+ expect(input.slug).toBe("acme-slack");
83
+ expect(input.labels).toEqual({ team: "support" });
84
+ // The other provider arm stays absent — the oneof is preserved.
85
+ expect(input.whatsapp).toBeUndefined();
86
+ });
87
+ });
@@ -13,6 +13,7 @@ import { ModelSelector } from "../models/ModelSelector.js";
13
13
  import { HarnessSelector } from "../models/HarnessSelector.js";
14
14
  import type { HarnessOption } from "../models/harness.js";
15
15
  import type { ServiceTierOption } from "../models/service-tier.js";
16
+ import type { ThinkingModeOption } from "../models/thinking-mode.js";
16
17
  import { InteractionModePicker, type InteractionModeOption } from "./InteractionModePicker.js";
17
18
  import {
18
19
  PaperclipIcon,
@@ -53,6 +54,10 @@ export interface ComposerToolbarProps {
53
54
  readonly serviceTier?: ServiceTierOption;
54
55
  /** Enables the fast-tier switch inside the model selector's options area. */
55
56
  readonly onServiceTierChange?: (tier: ServiceTierOption) => void;
57
+ /** Current thinking mode for the selected model (#772). */
58
+ readonly thinkingMode?: ThinkingModeOption;
59
+ /** Enables the thinking switch inside the model selector's options area. */
60
+ readonly onThinkingModeChange?: (mode: ThinkingModeOption) => void;
56
61
 
57
62
  // -- Right group: Secondary actions (icon-only) ---------------------------
58
63
 
@@ -128,6 +133,8 @@ export function ComposerToolbar({
128
133
  onModelChange,
129
134
  serviceTier,
130
135
  onServiceTierChange,
136
+ thinkingMode,
137
+ onThinkingModeChange,
131
138
  }: ComposerToolbarProps) {
132
139
  const showHarnessSeparate = showHarnessSelector && !showModelSelector;
133
140
 
@@ -161,6 +168,8 @@ export function ComposerToolbar({
161
168
  onHarnessChange={showHarnessSelector ? onHarnessChange : undefined}
162
169
  serviceTier={serviceTier}
163
170
  onServiceTierChange={onServiceTierChange}
171
+ thinkingMode={thinkingMode}
172
+ onThinkingModeChange={onThinkingModeChange}
164
173
  disabled={disabled}
165
174
  />
166
175
  )}
@@ -8,6 +8,7 @@ import { ComposerToolbar } from "./ComposerToolbar.js";
8
8
  import { type ConfigureMenuItem } from "./ConfigureMenu.js";
9
9
  import type { HarnessOption } from "../models/harness.js";
10
10
  import { FAST_SERVICE_TIER, type ServiceTierOption } from "../models/service-tier.js";
11
+ import type { ThinkingModeOption } from "../models/thinking-mode.js";
11
12
  import type { InteractionModeOption } from "./InteractionModePicker.js";
12
13
  import { parseModelKey } from "../models/registry.js";
13
14
  import { useModelRegistry } from "../models/useModelRegistry.js";
@@ -164,6 +165,14 @@ export interface SessionComposerSubmitContext {
164
165
  * Pass to execution creation as `execution_config.service_tier`.
165
166
  */
166
167
  readonly serviceTier?: ServiceTierOption;
168
+ /**
169
+ * Thinking mode the user actively selected for this execution
170
+ * (stigmer/stigmer#772). Only ever `"enabled"` — an untouched mode means
171
+ * "platform default" and is carried as `undefined`, the same
172
+ * unspecified-vs-explicit contract as {@link serviceTier}.
173
+ * Pass to execution creation as `execution_config.thinking_mode`.
174
+ */
175
+ readonly thinkingMode?: ThinkingModeOption;
167
176
  /**
168
177
  * Workspace-relative file paths the user referenced via drag-to-reference.
169
178
  *
@@ -663,6 +672,9 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
663
672
  // selection below (#663) — state here records the user's intent only.
664
673
  const [serviceTier, setServiceTier] = useState<ServiceTierOption>("standard");
665
674
 
675
+ // Thinking mode (#772): the tier's twin, same intent-vs-effective split.
676
+ const [thinkingMode, setThinkingMode] = useState<ThinkingModeOption>("disabled");
677
+
666
678
  // Active harness mirror: controlled hosts (both viewers) keep the
667
679
  // `harness` prop current, but with `showHarnessSelector` the dropdown
668
680
  // lives inside ModelSelector and an uncontrolled host would leave the
@@ -837,10 +849,26 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
837
849
  ? "fast"
838
850
  : "standard";
839
851
 
840
- return { modelId: effectiveModelId, model: effectiveModel, serviceTier: effectiveServiceTier };
852
+ // "enabled" rides under the identical rule, keyed on the capability
853
+ // (cursor-harness models only — v1 has no native thinking mapping, #772).
854
+ const effectiveThinkingMode: ThinkingModeOption =
855
+ thinkingMode === "enabled"
856
+ && effectiveModel !== undefined
857
+ && effectiveModel.harness === "cursor"
858
+ && effectiveModel.thinkingCapable === true
859
+ ? "enabled"
860
+ : "disabled";
861
+
862
+ return {
863
+ modelId: effectiveModelId,
864
+ model: effectiveModel,
865
+ serviceTier: effectiveServiceTier,
866
+ thinkingMode: effectiveThinkingMode,
867
+ };
841
868
  }, [
842
869
  modelId,
843
870
  serviceTier,
871
+ thinkingMode,
844
872
  showModelSelector,
845
873
  isRegistryLoading,
846
874
  registryGetByKey,
@@ -1054,15 +1082,19 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
1054
1082
  // explicit distinction is load-bearing telemetry (#357).
1055
1083
  const submitServiceTier =
1056
1084
  effective.serviceTier === "fast" ? effective.serviceTier : undefined;
1085
+ // The tier's #772 twin: only an in-effect "enabled" is carried.
1086
+ const submitThinkingMode =
1087
+ effective.thinkingMode === "enabled" ? effective.thinkingMode : undefined;
1057
1088
 
1058
1089
  const context: SessionComposerSubmitContext | undefined =
1059
1090
  hasEnv || hasAttachments || effectiveMode || hasFileRefs || buildFromPlan
1060
- || submitServiceTier
1091
+ || submitServiceTier || submitThinkingMode
1061
1092
  ? {
1062
1093
  runtimeEnv: hasEnv ? env : undefined,
1063
1094
  attachments: hasAttachments ? attachmentInputs : undefined,
1064
1095
  interactionMode: effectiveMode,
1065
1096
  serviceTier: submitServiceTier,
1097
+ thinkingMode: submitThinkingMode,
1066
1098
  workspaceFileRefs: hasFileRefs ? [...fileRefs.refs] : undefined,
1067
1099
  buildFromPlan,
1068
1100
  }
@@ -1894,6 +1926,8 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
1894
1926
  onModelChange={handleModelChange}
1895
1927
  serviceTier={serviceTier}
1896
1928
  onServiceTierChange={setServiceTier}
1929
+ thinkingMode={thinkingMode}
1930
+ onThinkingModeChange={setThinkingMode}
1897
1931
  />
1898
1932
  </div>
1899
1933
  </div>