@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,176 @@
1
+ import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
2
+ import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
3
+ import { type ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { StigmerContext } from "../../context";
6
+ import { ModelRegistryContext } from "../../models/ModelRegistryContext";
7
+ import type { ModelInfo } from "../../models/registry";
8
+ import { SessionComposer } from "../SessionComposer";
9
+
10
+ // Shim ResizeObserver for Base UI's positioner (the
11
+ // scheduleCreation.test.tsx pattern).
12
+ beforeAll(() => {
13
+ if (!("ResizeObserver" in globalThis)) {
14
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
15
+ class {
16
+ observe() {}
17
+ unobserve() {}
18
+ disconnect() {}
19
+ };
20
+ }
21
+ });
22
+
23
+ /**
24
+ * End-to-end composer wiring for the thinking mode (stigmer/stigmer#772) —
25
+ * the SessionComposer-serviceTier twin. The toolbar's ModelSelector toggle
26
+ * must reach composer state, and an active enabled selection must ride the
27
+ * submit context to execution create. This seam (SessionComposer →
28
+ * ComposerToolbar props → submit context) is exactly where the #357 tier
29
+ * toggle originally shipped dead while every layer unit-tested green.
30
+ */
31
+
32
+ const THINKING_CAPABLE: ModelInfo = {
33
+ modelId: "claude-haiku-4-5",
34
+ provider: "anthropic",
35
+ displayName: "Haiku 4.5",
36
+ shortDescription: "Fast Claude with optional thinking",
37
+ speedTier: "fast",
38
+ costTier: "economy",
39
+ harness: "cursor",
40
+ featured: true,
41
+ serviceTiers: [],
42
+ thinkingCapable: true,
43
+ };
44
+
45
+ const NOT_CAPABLE: ModelInfo = {
46
+ modelId: "composer-2.5",
47
+ provider: "cursor",
48
+ displayName: "Composer 2.5",
49
+ shortDescription: "Fast agentic model",
50
+ speedTier: "fastest",
51
+ costTier: "economy",
52
+ harness: "cursor",
53
+ featured: true,
54
+ serviceTiers: ["fast"],
55
+ thinkingCapable: false,
56
+ };
57
+
58
+ function createMinimalStigmerMock(): Stigmer {
59
+ return {
60
+ agentExecution: { uploadAttachment: vi.fn() },
61
+ environment: { getPersonal: vi.fn().mockResolvedValue(null) },
62
+ baseUrl: "http://localhost:8080",
63
+ getAuthCredential: vi.fn().mockResolvedValue("test-token"),
64
+ config: {
65
+ baseUrl: "http://localhost:8080",
66
+ getAccessToken: vi.fn().mockResolvedValue(""),
67
+ },
68
+ } as unknown as Stigmer;
69
+ }
70
+
71
+ function createWrapper(client: Stigmer) {
72
+ return function Wrapper({ children }: { children: ReactNode }) {
73
+ return (
74
+ <StigmerContext.Provider value={client}>
75
+ <ModelRegistryContext.Provider
76
+ value={{
77
+ models: [THINKING_CAPABLE, NOT_CAPABLE],
78
+ isLoading: false,
79
+ error: null,
80
+ refetch: vi.fn(),
81
+ }}
82
+ >
83
+ {children}
84
+ </ModelRegistryContext.Provider>
85
+ </StigmerContext.Provider>
86
+ );
87
+ };
88
+ }
89
+
90
+ function renderComposer(defaultModelId = "claude-haiku-4-5") {
91
+ const client = createMinimalStigmerMock();
92
+ const onSubmit = vi.fn();
93
+
94
+ const result = render(
95
+ <SessionComposer
96
+ onSubmit={onSubmit}
97
+ harness="cursor"
98
+ defaultModelId={defaultModelId}
99
+ />,
100
+ { wrapper: createWrapper(client) },
101
+ );
102
+
103
+ return { ...result, onSubmit };
104
+ }
105
+
106
+ function submitMessage(message: string) {
107
+ const textarea = screen.getByRole("textbox");
108
+ fireEvent.change(textarea, { target: { value: message } });
109
+ fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false });
110
+ }
111
+
112
+ /**
113
+ * Open the model selector popover (Base UI portals mount asynchronously in
114
+ * jsdom — the switch is awaited) and flip the thinking switch in the
115
+ * options area, then close the popover (toggling keeps it open).
116
+ */
117
+ async function toggleThinking(triggerName: RegExp = /Haiku 4\.5/) {
118
+ fireEvent.click(screen.getByRole("button", { name: triggerName }));
119
+ const toggle = await screen.findByRole("switch", { name: "Thinking" });
120
+ fireEvent.click(toggle);
121
+ fireEvent.keyDown(document.activeElement ?? document.body, { key: "Escape" });
122
+ }
123
+
124
+ afterEach(cleanup);
125
+
126
+ describe("SessionComposer — thinking mode submit contract", () => {
127
+ it("renders the thinking switch through the toolbar for a capable model", async () => {
128
+ renderComposer();
129
+
130
+ fireEvent.click(screen.getByRole("button", { name: /Haiku 4\.5/ }));
131
+ expect(await screen.findByRole("switch", { name: "Thinking" })).toBeTruthy();
132
+ });
133
+
134
+ it("renders no thinking switch for a model without the capability", async () => {
135
+ renderComposer("composer-2.5");
136
+
137
+ fireEvent.click(screen.getByRole("button", { name: /Composer 2\.5/ }));
138
+ // The fast switch proves the options area itself rendered — only the
139
+ // thinking row must be absent (no dead controls).
140
+ await screen.findByRole("switch", { name: "Fast tier" });
141
+ expect(screen.queryByRole("switch", { name: "Thinking" })).toBeNull();
142
+ });
143
+
144
+ it("carries thinkingMode 'enabled' on the submit context after toggling", async () => {
145
+ const { onSubmit } = renderComposer();
146
+
147
+ await toggleThinking();
148
+ submitMessage("Think hard about this");
149
+
150
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
151
+ const context = onSubmit.mock.calls[0][2];
152
+ expect(context?.thinkingMode).toBe("enabled");
153
+ });
154
+
155
+ it("leaves thinkingMode undefined when the switch is untouched", async () => {
156
+ const { onSubmit } = renderComposer();
157
+
158
+ submitMessage("Just answer");
159
+
160
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
161
+ const context = onSubmit.mock.calls[0][2];
162
+ expect(context?.thinkingMode).toBeUndefined();
163
+ });
164
+
165
+ it("leaves thinkingMode undefined after toggling on and back off", async () => {
166
+ const { onSubmit } = renderComposer();
167
+
168
+ await toggleThinking();
169
+ await toggleThinking();
170
+ submitMessage("Changed my mind");
171
+
172
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
173
+ const context = onSubmit.mock.calls[0][2];
174
+ expect(context?.thinkingMode).toBeUndefined();
175
+ });
176
+ });
@@ -10,7 +10,7 @@ import { Button } from "../button/index.js";
10
10
  import { StateBadge } from "./badges.js";
11
11
  import { CursorAccountEditor } from "./CursorAccountEditor.js";
12
12
  import { CursorAccountsAccessNotice } from "./CursorAccountsAccessNotice.js";
13
- import { deriveCoverage } from "./cursor-account-coverage.js";
13
+ import { deriveCoverage, derivePoolHealth } from "./cursor-account-coverage.js";
14
14
  import { formatSyncTime } from "./cursor-account-format.js";
15
15
  import { MemberKeysPanel } from "./MemberKeysPanel.js";
16
16
  import { useCursorAccounts } from "./useCursorAccounts.js";
@@ -119,11 +119,38 @@ export function CursorAccountsConsole({ className }: CursorAccountsConsoleProps)
119
119
  }
120
120
 
121
121
  const accounts = list.accounts?.accounts ?? [];
122
+ const health = derivePoolHealth(accounts);
122
123
 
123
124
  return (
124
125
  <div className={cn("stg:space-y-3", className)}>
125
126
  <div className="stg:flex stg:items-center stg:justify-between stg:gap-3">
126
- <h3 className="stg:text-sm stg:font-semibold stg:text-foreground">Cursor accounts</h3>
127
+ <div>
128
+ <h3 className="stg:text-sm stg:font-semibold stg:text-foreground">Cursor accounts</h3>
129
+ {/* Fleet health in one line (server-computed routability): the
130
+ 2026-08-15 drain hid behind per-account enabled counts — this
131
+ is the fact the operator needed on THIS screen. */}
132
+ {accounts.length > 0 && (
133
+ <p
134
+ className={cn(
135
+ "stg:text-[11px]",
136
+ health.totalRoutableKeys === 0
137
+ ? "stg:text-destructive"
138
+ : "stg:text-muted-foreground",
139
+ )}
140
+ role={health.totalRoutableKeys === 0 ? "alert" : undefined}
141
+ >
142
+ {health.totalRoutableKeys === 0
143
+ ? "No routable execution key anywhere - executions on API-pool models are failing"
144
+ : `${health.totalRoutableKeys} routable ${health.totalRoutableKeys === 1 ? "key" : "keys"} across ${health.totalAccounts} ${health.totalAccounts === 1 ? "account" : "accounts"}`}
145
+ {health.drainedAccounts > 0
146
+ ? ` · ${health.drainedAccounts} ${health.drainedAccounts === 1 ? "account" : "accounts"} drained`
147
+ : ""}
148
+ {health.guardTrippedKeys > 0
149
+ ? ` · ${health.guardTrippedKeys} ${health.guardTrippedKeys === 1 ? "key" : "keys"} usage-drained`
150
+ : ""}
151
+ </p>
152
+ )}
153
+ </div>
127
154
  <Button size="sm" onClick={() => setFlow({ phase: "create" })}>
128
155
  Add account
129
156
  </Button>
@@ -137,7 +164,7 @@ export function CursorAccountsConsole({ className }: CursorAccountsConsoleProps)
137
164
  <span>Account</span>
138
165
  <span>Orgs</span>
139
166
  <span>Status</span>
140
- <span>Execution keys</span>
167
+ <span>Routable keys</span>
141
168
  <span className="stg:text-right">Last synced</span>
142
169
  </div>
143
170
  {accounts.length === 0 ? (
@@ -214,8 +241,16 @@ function AccountRow({
214
241
  />
215
242
  </span>
216
243
  <span>
217
- {summary.enabledKeyCount > 0 ? (
218
- <span className="stg:text-muted-foreground">{summary.enabledKeyCount}</span>
244
+ {/* Routability, not enabled-ness: what key selection would DO.
245
+ "Drained" (usage guard) is recoverable at the cycle reset;
246
+ "Not routable" (no enabled key, or owners removed) needs
247
+ operator action. */}
248
+ {summary.routableKeyCount > 0 ? (
249
+ <span className="stg:text-muted-foreground">
250
+ {summary.routableKeyCount} of {summary.enabledKeyCount}
251
+ </span>
252
+ ) : summary.guardTrippedKeyCount > 0 ? (
253
+ <StateBadge tone="warn" label="Drained" />
219
254
  ) : (
220
255
  <StateBadge tone="warn" label="Not routable" />
221
256
  )}
@@ -86,6 +86,7 @@ function scenarSummary() {
86
86
  return create(CursorAccountSummarySchema, {
87
87
  account: scenarAccount(),
88
88
  enabledKeyCount: 1,
89
+ routableKeyCount: 1,
89
90
  lastSyncedAt: timestampFromDate(new Date("2026-07-22T12:00:00Z")),
90
91
  });
91
92
  }
@@ -194,6 +195,37 @@ describe("CursorAccountsConsole", () => {
194
195
  expect(screen.getByText("empty team")).toBeTruthy();
195
196
  expect(screen.getByText("Not routable")).toBeTruthy();
196
197
  expect(screen.getByText("shared pool")).toBeTruthy();
198
+ // Routable count renders as "N of M" (server-computed routability),
199
+ // and the fleet-health line aggregates it.
200
+ expect(screen.getByText("1 of 1")).toBeTruthy();
201
+ expect(screen.getByText(/1 routable key across 2 accounts/)).toBeTruthy();
202
+ });
203
+
204
+ it("surfaces a fully guard-drained account and the fleet drain alert", async () => {
205
+ // The 2026-08-15 shape: enabled keys exist, none routable — the one
206
+ // key left is usage-guard-tripped. The row must read "Drained" (a
207
+ // cycle-reset condition), not the misleading enabled count, and with
208
+ // zero routable keys fleet-wide the health line must alarm.
209
+ const drained = create(CursorAccountSummarySchema, {
210
+ account: scenarAccount({
211
+ accountId: "acc-3",
212
+ displayName: "drained team",
213
+ orgIds: [],
214
+ }),
215
+ enabledKeyCount: 1,
216
+ routableKeyCount: 0,
217
+ guardTrippedKeyCount: 1,
218
+ });
219
+ const client = createMockStigmer({
220
+ listAccounts: vi.fn().mockResolvedValue({ accounts: [drained] }),
221
+ });
222
+ render(<CursorAccountsConsole />, { wrapper: wrapper(client) });
223
+
224
+ await waitFor(() => expect(screen.getByText("drained team")).toBeTruthy());
225
+ expect(screen.getByText("Drained")).toBeTruthy();
226
+ expect(
227
+ screen.getByText(/No routable execution key anywhere/),
228
+ ).toBeTruthy();
197
229
  });
198
230
 
199
231
  it("shows the designed access notice on PERMISSION_DENIED", async () => {
@@ -7,12 +7,14 @@ import {
7
7
  CursorTeamMemberSchema,
8
8
  } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/cursor_account_pb";
9
9
  import {
10
+ CursorAccountSummarySchema,
10
11
  CursorAccountViewSchema,
11
12
  CursorMemberKeyState,
12
13
  CursorMemberKeyViewSchema,
13
14
  CursorTeamMemberViewSchema,
14
15
  } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/io_pb";
15
- import { deriveCoverage } from "../cursor-account-coverage";
16
+ import { CursorAccountSchema } from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/cursor_account_pb";
17
+ import { deriveCoverage, derivePoolHealth } from "../cursor-account-coverage";
16
18
 
17
19
  function keyView(keyId: string, state: CursorMemberKeyState) {
18
20
  return create(CursorMemberKeyViewSchema, {
@@ -71,3 +73,58 @@ describe("deriveCoverage", () => {
71
73
  expect(coverage.onTeamWithKey).toEqual([]);
72
74
  });
73
75
  });
76
+
77
+ describe("derivePoolHealth", () => {
78
+ function summary(opts: {
79
+ enabled?: boolean;
80
+ enabledKeyCount: number;
81
+ routableKeyCount: number;
82
+ guardTrippedKeyCount?: number;
83
+ }) {
84
+ return create(CursorAccountSummarySchema, {
85
+ account: create(CursorAccountSchema, {
86
+ accountId: "acc",
87
+ enabled: opts.enabled ?? true,
88
+ }),
89
+ enabledKeyCount: opts.enabledKeyCount,
90
+ routableKeyCount: opts.routableKeyCount,
91
+ guardTrippedKeyCount: opts.guardTrippedKeyCount ?? 0,
92
+ });
93
+ }
94
+
95
+ it("reproduces the 2026-08-15 drain as a one-line fact", () => {
96
+ // Four accounts with enabled keys but zero routable (guard-tripped or
97
+ // owner-removed), one fresh account with a single routable key — the
98
+ // fleet state the per-account enabled counts hid.
99
+ const health = derivePoolHealth([
100
+ summary({ enabledKeyCount: 1, routableKeyCount: 0, guardTrippedKeyCount: 1 }),
101
+ summary({ enabledKeyCount: 10, routableKeyCount: 0 }),
102
+ summary({ enabledKeyCount: 1, routableKeyCount: 0 }),
103
+ summary({ enabledKeyCount: 0, routableKeyCount: 0 }),
104
+ summary({ enabledKeyCount: 2, routableKeyCount: 1, guardTrippedKeyCount: 0 }),
105
+ ]);
106
+
107
+ expect(health.totalAccounts).toBe(5);
108
+ expect(health.totalRoutableKeys).toBe(1);
109
+ // The zero-key account is not "drained" — it never had capacity.
110
+ expect(health.drainedAccounts).toBe(3);
111
+ expect(health.guardTrippedKeys).toBe(1);
112
+ });
113
+
114
+ it("does not count disabled accounts as drained (disable means drain-by-choice)", () => {
115
+ const health = derivePoolHealth([
116
+ summary({ enabled: false, enabledKeyCount: 3, routableKeyCount: 0 }),
117
+ ]);
118
+
119
+ expect(health.drainedAccounts).toBe(0);
120
+ });
121
+
122
+ it("is all zeros for an empty list", () => {
123
+ const health = derivePoolHealth([]);
124
+
125
+ expect(health.totalAccounts).toBe(0);
126
+ expect(health.totalRoutableKeys).toBe(0);
127
+ expect(health.drainedAccounts).toBe(0);
128
+ expect(health.guardTrippedKeys).toBe(0);
129
+ });
130
+ });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CursorMemberKeyState,
3
+ type CursorAccountSummary,
3
4
  type CursorAccountView,
4
5
  type CursorMemberKeyView,
5
6
  type CursorTeamMemberView,
@@ -69,3 +70,57 @@ export function deriveCoverage(view: CursorAccountView): CursorAccountCoverage {
69
70
  unclassified: [],
70
71
  };
71
72
  }
73
+
74
+ /**
75
+ * Fleet-wide health rolled up from the list summaries — the console's
76
+ * answer to "can the platform serve right now, and from where?". Pure
77
+ * aggregation of the server-computed routable/guard counts (the same
78
+ * routability rule key selection uses); this module must never
79
+ * re-derive routability client-side.
80
+ *
81
+ * <p>Why this exists: in the 2026-08-15 pool drain, the list showed
82
+ * five accounts with healthy-looking enabled-key counts while exactly
83
+ * one guard-tripped key could serve — the operator had to open every
84
+ * account detail to see it. The rollup makes fleet exhaustion a
85
+ * one-line fact.
86
+ */
87
+ export interface CursorPoolHealth {
88
+ /** Total accounts in the list. */
89
+ readonly totalAccounts: number;
90
+ /** Keys the routability rule would select for a new session, fleet-wide. */
91
+ readonly totalRoutableKeys: number;
92
+ /**
93
+ * Enabled accounts with enabled keys but zero routable ones — every
94
+ * key is dead (owner removed) or usage-drained. These are the rows
95
+ * that look healthy at a glance and are not.
96
+ */
97
+ readonly drainedAccounts: number;
98
+ /** Enabled keys excluded only by the usage guard, fleet-wide. */
99
+ readonly guardTrippedKeys: number;
100
+ }
101
+
102
+ /** Aggregate the server-computed summary counts across the account list. */
103
+ export function derivePoolHealth(
104
+ summaries: readonly CursorAccountSummary[],
105
+ ): CursorPoolHealth {
106
+ let totalRoutableKeys = 0;
107
+ let drainedAccounts = 0;
108
+ let guardTrippedKeys = 0;
109
+ for (const summary of summaries) {
110
+ totalRoutableKeys += summary.routableKeyCount;
111
+ guardTrippedKeys += summary.guardTrippedKeyCount;
112
+ if (
113
+ summary.account?.enabled === true
114
+ && summary.enabledKeyCount > 0
115
+ && summary.routableKeyCount === 0
116
+ ) {
117
+ drainedAccounts += 1;
118
+ }
119
+ }
120
+ return {
121
+ totalAccounts: summaries.length,
122
+ totalRoutableKeys,
123
+ drainedAccounts,
124
+ guardTrippedKeys,
125
+ };
126
+ }
@@ -14,6 +14,7 @@ import { useStigmer } from "../hooks.js";
14
14
  import { toError } from "../internal/toError.js";
15
15
  import { toProtoInteractionMode } from "../composer/interaction-mode.js";
16
16
  import { toProtoServiceTier, type ServiceTierOption } from "../models/service-tier.js";
17
+ import { toProtoThinkingMode, type ThinkingModeOption } from "../models/thinking-mode.js";
17
18
  import { toProtoHarness, type HarnessOption } from "../models/harness.js";
18
19
  import {
19
20
  toProtoExecutionTarget,
@@ -138,6 +139,20 @@ export interface SharedAgentExecutionFields {
138
139
  * Maps to `ExecutionConfig.service_tier` in the proto.
139
140
  */
140
141
  readonly serviceTier?: ServiceTierOption;
142
+ /**
143
+ * Thinking mode for this execution's model calls (stigmer/stigmer#772).
144
+ *
145
+ * - `"disabled"` (default): the model's base variant, pinned explicitly —
146
+ * never the provider account default.
147
+ * - `"enabled"`: the model's extended-reasoning variant, billed at base
148
+ * per-token rates (reasoning tokens bill as output). Valid only for
149
+ * cursor-harness models whose registry entry declares the thinking
150
+ * capability — the backend refuses the create otherwise, so gate the
151
+ * option on `ModelInfo.thinkingCapable`.
152
+ *
153
+ * Maps to `ExecutionConfig.thinking_mode` in the proto.
154
+ */
155
+ readonly thinkingMode?: ThinkingModeOption;
141
156
  /**
142
157
  * Marks this execution as a Build-from-plan turn: the user approved a
143
158
  * plan from a prior Plan-mode execution and asked the agent to implement
@@ -318,6 +333,7 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
318
333
  input.modelName ||
319
334
  input.interactionMode ||
320
335
  input.serviceTier ||
336
+ input.thinkingMode ||
321
337
  input.structuredOutputSchema ||
322
338
  input.buildFromPlan;
323
339
  const executionConfig = hasConfig
@@ -329,6 +345,9 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
329
345
  ...(input.serviceTier
330
346
  ? { serviceTier: toProtoServiceTier(input.serviceTier) }
331
347
  : {}),
348
+ ...(input.thinkingMode
349
+ ? { thinkingMode: toProtoThinkingMode(input.thinkingMode) }
350
+ : {}),
332
351
  ...(input.structuredOutputSchema
333
352
  ? { structuredOutputSchema: input.structuredOutputSchema }
334
353
  : {}),