@stigmer/react 3.6.0 → 3.8.0

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 (260) hide show
  1. package/billing/CreditLedgerTable.d.ts.map +1 -1
  2. package/billing/CreditLedgerTable.js +2 -7
  3. package/billing/CreditLedgerTable.js.map +1 -1
  4. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  5. package/channel/AgentChannelsPanel.js +21 -2
  6. package/channel/AgentChannelsPanel.js.map +1 -1
  7. package/channel/ChannelConversationsDialog.d.ts +12 -4
  8. package/channel/ChannelConversationsDialog.d.ts.map +1 -1
  9. package/channel/ChannelConversationsDialog.js +18 -24
  10. package/channel/ChannelConversationsDialog.js.map +1 -1
  11. package/channel/providers.d.ts +23 -0
  12. package/channel/providers.d.ts.map +1 -1
  13. package/channel/providers.js +4 -0
  14. package/channel/providers.js.map +1 -1
  15. package/channel/useSaveAgentChannel.d.ts +5 -4
  16. package/channel/useSaveAgentChannel.d.ts.map +1 -1
  17. package/channel/useSaveAgentChannel.js +18 -4
  18. package/channel/useSaveAgentChannel.js.map +1 -1
  19. package/composer/ComposerToolbar.d.ts +6 -1
  20. package/composer/ComposerToolbar.d.ts.map +1 -1
  21. package/composer/ComposerToolbar.js +2 -2
  22. package/composer/ComposerToolbar.js.map +1 -1
  23. package/composer/SessionComposer.d.ts +9 -0
  24. package/composer/SessionComposer.d.ts.map +1 -1
  25. package/composer/SessionComposer.js +13 -2
  26. package/composer/SessionComposer.js.map +1 -1
  27. package/conversation/ConversationAttentionBanner.d.ts +22 -0
  28. package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
  29. package/conversation/ConversationAttentionBanner.js +25 -0
  30. package/conversation/ConversationAttentionBanner.js.map +1 -0
  31. package/conversation/ConversationComposer.d.ts +41 -0
  32. package/conversation/ConversationComposer.d.ts.map +1 -0
  33. package/conversation/ConversationComposer.js +57 -0
  34. package/conversation/ConversationComposer.js.map +1 -0
  35. package/conversation/ConversationControlBanner.d.ts +52 -0
  36. package/conversation/ConversationControlBanner.d.ts.map +1 -0
  37. package/conversation/ConversationControlBanner.js +59 -0
  38. package/conversation/ConversationControlBanner.js.map +1 -0
  39. package/conversation/ConversationListPane.d.ts +68 -0
  40. package/conversation/ConversationListPane.d.ts.map +1 -0
  41. package/conversation/ConversationListPane.js +120 -0
  42. package/conversation/ConversationListPane.js.map +1 -0
  43. package/conversation/ConversationTimelineView.d.ts +46 -0
  44. package/conversation/ConversationTimelineView.d.ts.map +1 -0
  45. package/conversation/ConversationTimelineView.js +166 -0
  46. package/conversation/ConversationTimelineView.js.map +1 -0
  47. package/conversation/ConversationsWorkbench.d.ts +63 -0
  48. package/conversation/ConversationsWorkbench.d.ts.map +1 -0
  49. package/conversation/ConversationsWorkbench.js +165 -0
  50. package/conversation/ConversationsWorkbench.js.map +1 -0
  51. package/conversation/conversationPresentation.d.ts +137 -0
  52. package/conversation/conversationPresentation.d.ts.map +1 -0
  53. package/conversation/conversationPresentation.js +230 -0
  54. package/conversation/conversationPresentation.js.map +1 -0
  55. package/conversation/index.d.ts +26 -0
  56. package/conversation/index.d.ts.map +1 -0
  57. package/conversation/index.js +14 -0
  58. package/conversation/index.js.map +1 -0
  59. package/conversation/polling.d.ts +39 -0
  60. package/conversation/polling.d.ts.map +1 -0
  61. package/conversation/polling.js +39 -0
  62. package/conversation/polling.js.map +1 -0
  63. package/conversation/useConversation.d.ts +61 -0
  64. package/conversation/useConversation.d.ts.map +1 -0
  65. package/conversation/useConversation.js +153 -0
  66. package/conversation/useConversation.js.map +1 -0
  67. package/conversation/useConversationList.d.ts +98 -0
  68. package/conversation/useConversationList.d.ts.map +1 -0
  69. package/conversation/useConversationList.js +200 -0
  70. package/conversation/useConversationList.js.map +1 -0
  71. package/conversation/useConversationParticipation.d.ts +63 -0
  72. package/conversation/useConversationParticipation.d.ts.map +1 -0
  73. package/conversation/useConversationParticipation.js +84 -0
  74. package/conversation/useConversationParticipation.js.map +1 -0
  75. package/conversation/useConversationTimeline.d.ts +71 -0
  76. package/conversation/useConversationTimeline.d.ts.map +1 -0
  77. package/conversation/useConversationTimeline.js +152 -0
  78. package/conversation/useConversationTimeline.js.map +1 -0
  79. package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
  80. package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
  81. package/conversation/useConversationsWantsHumanCount.js +40 -0
  82. package/conversation/useConversationsWantsHumanCount.js.map +1 -0
  83. package/execution/useCreateAgentExecution.d.ts +14 -0
  84. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  85. package/execution/useCreateAgentExecution.js +5 -0
  86. package/execution/useCreateAgentExecution.js.map +1 -1
  87. package/index.d.ts +6 -2
  88. package/index.d.ts.map +1 -1
  89. package/index.js +14 -5
  90. package/index.js.map +1 -1
  91. package/internal/Pagination.d.ts +15 -0
  92. package/internal/Pagination.d.ts.map +1 -0
  93. package/internal/Pagination.js +20 -0
  94. package/internal/Pagination.js.map +1 -0
  95. package/internal/tooltip.d.ts.map +1 -1
  96. package/internal/tooltip.js +7 -1
  97. package/internal/tooltip.js.map +1 -1
  98. package/internal/useAutoScroll.d.ts +25 -4
  99. package/internal/useAutoScroll.d.ts.map +1 -1
  100. package/internal/useAutoScroll.js +55 -15
  101. package/internal/useAutoScroll.js.map +1 -1
  102. package/internal/useFetch.d.ts.map +1 -1
  103. package/internal/useFetch.js +14 -4
  104. package/internal/useFetch.js.map +1 -1
  105. package/models/ModelSelector.d.ts +45 -1
  106. package/models/ModelSelector.d.ts.map +1 -1
  107. package/models/ModelSelector.js +49 -6
  108. package/models/ModelSelector.js.map +1 -1
  109. package/models/index.d.ts +2 -0
  110. package/models/index.d.ts.map +1 -1
  111. package/models/index.js +1 -0
  112. package/models/index.js.map +1 -1
  113. package/models/registry.d.ts +11 -0
  114. package/models/registry.d.ts.map +1 -1
  115. package/models/registry.js +3 -0
  116. package/models/registry.js.map +1 -1
  117. package/models/service-tier.d.ts +27 -0
  118. package/models/service-tier.d.ts.map +1 -0
  119. package/models/service-tier.js +34 -0
  120. package/models/service-tier.js.map +1 -0
  121. package/package.json +4 -4
  122. package/schedule/ScheduleDetailView.d.ts +31 -8
  123. package/schedule/ScheduleDetailView.d.ts.map +1 -1
  124. package/schedule/ScheduleDetailView.js +441 -29
  125. package/schedule/ScheduleDetailView.js.map +1 -1
  126. package/schedule/ScheduleForm.d.ts +7 -4
  127. package/schedule/ScheduleForm.d.ts.map +1 -1
  128. package/schedule/ScheduleForm.js +94 -8
  129. package/schedule/ScheduleForm.js.map +1 -1
  130. package/schedule/ScheduleRunsTable.d.ts +72 -0
  131. package/schedule/ScheduleRunsTable.d.ts.map +1 -0
  132. package/schedule/ScheduleRunsTable.js +161 -0
  133. package/schedule/ScheduleRunsTable.js.map +1 -0
  134. package/schedule/index.d.ts +6 -0
  135. package/schedule/index.d.ts.map +1 -1
  136. package/schedule/index.js +3 -0
  137. package/schedule/index.js.map +1 -1
  138. package/schedule/useScheduleRuns.d.ts +44 -0
  139. package/schedule/useScheduleRuns.d.ts.map +1 -0
  140. package/schedule/useScheduleRuns.js +48 -0
  141. package/schedule/useScheduleRuns.js.map +1 -0
  142. package/schedule/useTriggerSchedule.d.ts +23 -10
  143. package/schedule/useTriggerSchedule.d.ts.map +1 -1
  144. package/schedule/useTriggerSchedule.js +28 -9
  145. package/schedule/useTriggerSchedule.js.map +1 -1
  146. package/schedule/useUpdateScheduleSpec.d.ts +49 -0
  147. package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
  148. package/schedule/useUpdateScheduleSpec.js +70 -0
  149. package/schedule/useUpdateScheduleSpec.js.map +1 -0
  150. package/session/useNewSessionFlow.d.ts.map +1 -1
  151. package/session/useNewSessionFlow.js +1 -0
  152. package/session/useNewSessionFlow.js.map +1 -1
  153. package/session/useSessionConversation.d.ts +11 -0
  154. package/session/useSessionConversation.d.ts.map +1 -1
  155. package/session/useSessionConversation.js +1 -0
  156. package/session/useSessionConversation.js.map +1 -1
  157. package/session/useSessionPageFlow.d.ts.map +1 -1
  158. package/session/useSessionPageFlow.js +1 -0
  159. package/session/useSessionPageFlow.js.map +1 -1
  160. package/sharing/useSaveAgentShare.d.ts +10 -1
  161. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  162. package/sharing/useSaveAgentShare.js +9 -0
  163. package/sharing/useSaveAgentShare.js.map +1 -1
  164. package/sidebar/WorkspaceSidebar.d.ts +12 -4
  165. package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
  166. package/sidebar/WorkspaceSidebar.js +19 -7
  167. package/sidebar/WorkspaceSidebar.js.map +1 -1
  168. package/src/billing/CreditLedgerTable.tsx +3 -42
  169. package/src/channel/AgentChannelsPanel.tsx +33 -2
  170. package/src/channel/ChannelConversationsDialog.tsx +22 -25
  171. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
  172. package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
  173. package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
  174. package/src/channel/providers.ts +27 -0
  175. package/src/channel/useSaveAgentChannel.ts +18 -4
  176. package/src/composer/ComposerToolbar.tsx +9 -0
  177. package/src/composer/SessionComposer.tsx +24 -1
  178. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +157 -0
  179. package/src/conversation/ConversationAttentionBanner.tsx +77 -0
  180. package/src/conversation/ConversationComposer.tsx +156 -0
  181. package/src/conversation/ConversationControlBanner.tsx +193 -0
  182. package/src/conversation/ConversationListPane.tsx +459 -0
  183. package/src/conversation/ConversationTimelineView.tsx +435 -0
  184. package/src/conversation/ConversationsWorkbench.tsx +378 -0
  185. package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
  186. package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
  187. package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
  188. package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
  189. package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
  190. package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
  191. package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
  192. package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
  193. package/src/conversation/__tests__/a11y/harness.tsx +100 -0
  194. package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
  195. package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
  196. package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
  197. package/src/conversation/__tests__/useConversation.test.tsx +243 -0
  198. package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
  199. package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
  200. package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
  201. package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
  202. package/src/conversation/conversationPresentation.ts +305 -0
  203. package/src/conversation/index.ts +80 -0
  204. package/src/conversation/polling.ts +41 -0
  205. package/src/conversation/useConversation.ts +221 -0
  206. package/src/conversation/useConversationList.ts +328 -0
  207. package/src/conversation/useConversationParticipation.ts +178 -0
  208. package/src/conversation/useConversationTimeline.ts +244 -0
  209. package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
  210. package/src/execution/useCreateAgentExecution.ts +18 -0
  211. package/src/index.ts +76 -4
  212. package/src/internal/Pagination.tsx +72 -0
  213. package/src/internal/__tests__/fake-timer-slices.ts +29 -0
  214. package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
  215. package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
  216. package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
  217. package/src/internal/tooltip.tsx +6 -1
  218. package/src/internal/useAutoScroll.ts +66 -20
  219. package/src/internal/useFetch.ts +15 -4
  220. package/src/models/ModelSelector.tsx +133 -4
  221. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +256 -0
  222. package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
  223. package/src/models/index.ts +2 -0
  224. package/src/models/registry.ts +17 -0
  225. package/src/models/service-tier.ts +49 -0
  226. package/src/schedule/ScheduleDetailView.tsx +1179 -111
  227. package/src/schedule/ScheduleForm.tsx +218 -7
  228. package/src/schedule/ScheduleRunsTable.tsx +424 -0
  229. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
  230. package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
  231. package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
  232. package/src/schedule/index.ts +12 -0
  233. package/src/schedule/useScheduleRuns.ts +93 -0
  234. package/src/schedule/useTriggerSchedule.ts +39 -14
  235. package/src/schedule/useUpdateScheduleSpec.ts +108 -0
  236. package/src/session/useNewSessionFlow.ts +1 -0
  237. package/src/session/useSessionConversation.ts +12 -0
  238. package/src/session/useSessionPageFlow.ts +1 -0
  239. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
  240. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
  241. package/src/sharing/useSaveAgentShare.ts +19 -1
  242. package/src/sidebar/WorkspaceSidebar.tsx +62 -3
  243. package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
  244. package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
  245. package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
  246. package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
  247. package/src/workflow/starter-workflow-yaml.ts +2 -1
  248. package/styles.css +1 -1
  249. package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
  250. package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
  251. package/workflow/inspector/forms/AgentCallForm.js +87 -9
  252. package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
  253. package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
  254. package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
  255. package/workflow/inspector/tabs/RuntimeTab.js +31 -5
  256. package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
  257. package/workflow/starter-workflow-yaml.d.ts +1 -1
  258. package/workflow/starter-workflow-yaml.d.ts.map +1 -1
  259. package/workflow/starter-workflow-yaml.js +2 -1
  260. package/workflow/starter-workflow-yaml.js.map +1 -1
@@ -17,6 +17,7 @@ import type {
17
17
  WorkspaceEntryInput,
18
18
  } from "@stigmer/sdk";
19
19
  import { isTerminalPhase } from "../execution/execution-phases.js";
20
+ import type { ServiceTierOption } from "../models/service-tier.js";
20
21
  import { useStigmer } from "../hooks.js";
21
22
  import { toError } from "../internal/toError.js";
22
23
  import { useConversationStoreRef } from "../internal/store/index.js";
@@ -96,6 +97,16 @@ export interface SendFollowUpOptions {
96
97
  * - `"plan"`: read-only analysis, no file mutations.
97
98
  */
98
99
  readonly interactionMode?: "agent" | "plan";
100
+ /**
101
+ * Service tier for this execution's model calls (stigmer/stigmer#357).
102
+ *
103
+ * Only set when the user actively selected `"fast"`; `undefined` preserves
104
+ * the unspecified-vs-explicit distinction (unset resolves to standard on
105
+ * the platform side — never the provider account default).
106
+ *
107
+ * @see {@link CreateAgentExecutionInput.serviceTier}
108
+ */
109
+ readonly serviceTier?: ServiceTierOption;
99
110
  /**
100
111
  * Marks this execution as a Build-from-plan turn.
101
112
  *
@@ -642,6 +653,7 @@ export function useSessionConversation(
642
653
  runtimeEnv: options?.runtimeEnv,
643
654
  attachments: options?.attachments,
644
655
  interactionMode: options?.interactionMode,
656
+ serviceTier: options?.serviceTier,
645
657
  buildFromPlan: options?.buildFromPlan,
646
658
  autoApproveAll: options?.autoApproveAll,
647
659
  workspaceFileRefs: options?.workspaceFileRefs,
@@ -438,6 +438,7 @@ export function useSessionPageFlow(
438
438
  runtimeEnv,
439
439
  attachments: context?.attachments,
440
440
  interactionMode: context?.interactionMode,
441
+ serviceTier: context?.serviceTier,
441
442
  buildFromPlan: context?.buildFromPlan,
442
443
  // Sourced from the session-scoped preference set at the approval gate,
443
444
  // not from the composer (the pre-arm toggle was removed).
@@ -6,6 +6,7 @@ import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v
6
6
  import type { AgentShareInput } from "@stigmer/sdk";
7
7
  import { StigmerContext } from "../../context";
8
8
  import {
9
+ draftFromShare,
9
10
  sharingAudienceFromProto,
10
11
  useSaveAgentShare,
11
12
  type AgentShareDraft,
@@ -50,6 +51,7 @@ const FULL_DRAFT: AgentShareDraft = {
50
51
  conversationEnded: "This conversation has ended.",
51
52
  },
52
53
  environmentRefs: [{ org: "acme", slug: "github-org-shared" }],
54
+ runConfig: { modelName: "gpt-5-mini", maxCostUsd: 0.25 },
53
55
  };
54
56
 
55
57
  describe("useSaveAgentShare", () => {
@@ -88,6 +90,7 @@ describe("useSaveAgentShare", () => {
88
90
  expect(input.environmentRefs).toEqual([
89
91
  { org: "acme", slug: "github-org-shared" },
90
92
  ]);
93
+ expect(input.runConfig).toEqual({ modelName: "gpt-5-mini", maxCostUsd: 0.25 });
91
94
  });
92
95
 
93
96
  it("keeps the existing share's identity when editing (a renamed share is never forked)", async () => {
@@ -123,7 +126,15 @@ describe("useSaveAgentShare", () => {
123
126
 
124
127
  await act(() =>
125
128
  result.current.save(
126
- { ...FULL_DRAFT, enabled: false, audience: "org", environmentRefs: [] },
129
+ // An org-audience share carries no environmentRefs or runConfig
130
+ // (both are public-audience-only by the proto CEL rules).
131
+ {
132
+ ...FULL_DRAFT,
133
+ enabled: false,
134
+ audience: "org",
135
+ environmentRefs: [],
136
+ runConfig: undefined,
137
+ },
127
138
  null,
128
139
  ),
129
140
  );
@@ -138,6 +149,49 @@ describe("useSaveAgentShare", () => {
138
149
  expect(input.audience).toBe(AgentShareAudience.org);
139
150
  });
140
151
 
152
+ it("a pause/resume toggle built from draftFromShare carries run_config (never wipes an API-set override)", async () => {
153
+ const apply = vi.fn().mockResolvedValue({});
154
+ const client = createMockStigmer({ apply });
155
+ // A share whose run_config was set via CLI/API — the console list's
156
+ // toggle rebuilds the full spec from draftFromShare, so the override
157
+ // must ride through even though no console surface edits it.
158
+ const share = {
159
+ metadata: { id: "ash_1", org: "acme", slug: "support-agent", name: "Support Agent" },
160
+ spec: {
161
+ enabled: true,
162
+ allowedOrigins: ["https://example.com"],
163
+ environmentRefs: [],
164
+ runConfig: { modelName: "gpt-5-mini", maxCostUsd: 0.25, maxToolRounds: 8 },
165
+ },
166
+ } as unknown as AgentShare;
167
+
168
+ const { result } = renderHook(() => useSaveAgentShare(AGENT), {
169
+ wrapper: wrapper(client),
170
+ });
171
+
172
+ await act(() =>
173
+ result.current.save({ ...draftFromShare(share), enabled: false }, share),
174
+ );
175
+
176
+ const input = apply.mock.calls[0][0] as AgentShareInput;
177
+ expect(input.enabled).toBe(false);
178
+ expect(input.runConfig).toEqual({
179
+ modelName: "gpt-5-mini",
180
+ maxCostUsd: 0.25,
181
+ maxToolRounds: 8,
182
+ serviceTier: undefined,
183
+ });
184
+ });
185
+
186
+ it("draftFromShare leaves run_config undefined when the share carries none", () => {
187
+ expect(draftFromShare(null).runConfig).toBeUndefined();
188
+ const bare = {
189
+ metadata: { id: "ash_2", org: "acme", slug: "bare" },
190
+ spec: { enabled: true },
191
+ } as AgentShare;
192
+ expect(draftFromShare(bare).runConfig).toBeUndefined();
193
+ });
194
+
141
195
  it("maps the audience union to the proto enum, writing public explicitly", async () => {
142
196
  const apply = vi.fn().mockResolvedValue({});
143
197
  const client = createMockStigmer({ apply });
@@ -34,6 +34,7 @@ function makeDraft(overrides?: {
34
34
  allowedOrigins: [],
35
35
  messages: { rateLimited: "", unavailable: "", conversationEnded: "" },
36
36
  environmentRefs: overrides?.environmentRefs ?? [],
37
+ runConfig: undefined,
37
38
  };
38
39
  }
39
40
 
@@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
4
4
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
5
5
  import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
6
6
  import { AgentShareAudience } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/spec_pb";
7
- import type { ResourceRef } from "@stigmer/sdk";
7
+ import type { ResourceRef, RunConfigInput } from "@stigmer/sdk";
8
8
  import { useStigmer } from "../hooks.js";
9
9
  import { toError } from "../internal/toError.js";
10
10
 
@@ -57,6 +57,15 @@ export interface AgentShareDraft {
57
57
  * sessions carry no share linkage in Phase A — decision 011 addendum).
58
58
  */
59
59
  readonly environmentRefs: readonly ResourceRef[];
60
+ /**
61
+ * The owner's per-share execution override (model / cost cap / tool
62
+ * rounds / service tier), merged over the platform guest profile at
63
+ * run time — stigmer/stigmer#360. The console does not edit it (yet):
64
+ * it rides through every save opaquely so a toggle never wipes an
65
+ * override set via the CLI or API. Public-audience only (the same
66
+ * proto CEL rule as environmentRefs, for the same reason).
67
+ */
68
+ readonly runConfig: RunConfigInput | undefined;
60
69
  }
61
70
 
62
71
  /**
@@ -103,6 +112,14 @@ export function draftFromShare(share: AgentShare | null): AgentShareDraft {
103
112
  org: ref.org,
104
113
  slug: ref.slug,
105
114
  })),
115
+ runConfig: spec?.runConfig
116
+ ? {
117
+ modelName: spec.runConfig.modelName,
118
+ maxCostUsd: spec.runConfig.maxCostUsd,
119
+ maxToolRounds: spec.runConfig.maxToolRounds,
120
+ serviceTier: spec.runConfig.serviceTier,
121
+ }
122
+ : undefined,
106
123
  };
107
124
  }
108
125
 
@@ -229,6 +246,7 @@ export function useSaveAgentShare(
229
246
  org: ref.org,
230
247
  slug: ref.slug,
231
248
  })),
249
+ runConfig: draft.runConfig,
232
250
  });
233
251
  } catch (err) {
234
252
  setError(toError(err));
@@ -6,6 +6,7 @@ import {
6
6
  LayoutDashboard,
7
7
  Library,
8
8
  MessageSquare,
9
+ MessagesSquare,
9
10
  Workflow,
10
11
  type LucideIcon,
11
12
  } from "lucide-react";
@@ -29,7 +30,11 @@ import type { RenderSidebarLink } from "./types.js";
29
30
  // ---------------------------------------------------------------------------
30
31
 
31
32
  /** Primary navigation destinations in the workspace sidebar. */
32
- export type WorkspaceNavId = "new-session" | "dashboard" | "library";
33
+ export type WorkspaceNavId =
34
+ | "new-session"
35
+ | "dashboard"
36
+ | "conversations"
37
+ | "library";
33
38
 
34
39
  /**
35
40
  * Recent-activity data for the sidebar's Recents section — the return
@@ -66,6 +71,14 @@ export interface WorkspaceSidebarProps {
66
71
  * timestamp column, inside the row.
67
72
  */
68
73
  readonly renderEntryAccessory?: (entry: RecentActivityEntry) => ReactNode;
74
+ /**
75
+ * Count for the Conversations row's badge: how many conversations
76
+ * want a human right now (`useConversationsWantsHumanCount`). Data as
77
+ * props per the chrome contract — hosts fetch, deterministic hosts
78
+ * (tours) pass a fixture or omit. Hidden at `0`/omitted; the display
79
+ * caps at "99+" while the accessible name keeps the real number.
80
+ */
81
+ readonly conversationsBadgeCount?: number;
69
82
  /** Footer content — typically the host app's `UserMenu` wrapper. */
70
83
  readonly footer: ReactNode;
71
84
  /** Reflected as `aria-expanded` on the collapse toggle. @default true */
@@ -85,8 +98,8 @@ export interface WorkspaceSidebarProps {
85
98
 
86
99
  /**
87
100
  * The console's workspace-zone sidebar: org switcher, primary navigation
88
- * (New Session / Dashboard / Library), time-bucketed recent activity,
89
- * and a user footer.
101
+ * (New Session / Dashboard / Conversations / Library), time-bucketed
102
+ * recent activity, and a user footer.
90
103
  *
91
104
  * This is the same component the Stigmer web console and desktop app
92
105
  * render — hosts differ only in what they return from `renderLink`
@@ -117,6 +130,7 @@ export function WorkspaceSidebar({
117
130
  activeSessionId = null,
118
131
  activeExecutionId = null,
119
132
  renderEntryAccessory,
133
+ conversationsBadgeCount,
120
134
  footer,
121
135
  isOpen = true,
122
136
  onCollapse,
@@ -154,6 +168,22 @@ export function WorkspaceSidebar({
154
168
  active={activeNav === "dashboard"}
155
169
  renderLink={renderLink}
156
170
  />
171
+ {/* MessagesSquare, deliberately not MessageSquare — the singular
172
+ mark is the session glyph in Recents, and the two areas must
173
+ not read as one. */}
174
+ <PrimaryNavRow
175
+ id="conversations"
176
+ href="/conversations"
177
+ label="Conversations"
178
+ icon={MessagesSquare}
179
+ active={activeNav === "conversations"}
180
+ renderLink={renderLink}
181
+ accessory={
182
+ conversationsBadgeCount !== undefined && conversationsBadgeCount > 0 ? (
183
+ <WantsHumanBadge count={conversationsBadgeCount} />
184
+ ) : null
185
+ }
186
+ />
157
187
  <PrimaryNavRow
158
188
  id="library"
159
189
  href="/library"
@@ -204,6 +234,7 @@ function PrimaryNavRow({
204
234
  icon: Icon,
205
235
  active,
206
236
  renderLink,
237
+ accessory = null,
207
238
  }: {
208
239
  readonly id: WorkspaceNavId;
209
240
  readonly href: string;
@@ -211,6 +242,8 @@ function PrimaryNavRow({
211
242
  readonly icon: LucideIcon;
212
243
  readonly active: boolean;
213
244
  readonly renderLink: RenderSidebarLink;
245
+ /** Trailing row content (e.g. the Conversations count pill). */
246
+ readonly accessory?: ReactNode;
214
247
  }) {
215
248
  return (
216
249
  <div className="flex-none px-3 py-1">
@@ -224,6 +257,7 @@ function PrimaryNavRow({
224
257
  <>
225
258
  <Icon className="size-4 shrink-0" />
226
259
  {label}
260
+ {accessory}
227
261
  </>
228
262
  ),
229
263
  })}
@@ -231,6 +265,31 @@ function PrimaryNavRow({
231
265
  );
232
266
  }
233
267
 
268
+ /**
269
+ * The Conversations row's count pill: how many conversations want a
270
+ * human right now (DD-011 D-f — `needs_attention` OR awaiting-reply
271
+ * while human-held). The visible number caps at "99+"; the accessible
272
+ * name states the meaning with the real number, because a bare "104"
273
+ * read aloud after "Conversations" says nothing.
274
+ */
275
+ function WantsHumanBadge({ count }: { readonly count: number }) {
276
+ return (
277
+ <span className="ml-auto flex shrink-0 items-center">
278
+ <span
279
+ aria-hidden="true"
280
+ className="bg-sidebar-primary text-sidebar-primary-foreground rounded-full px-1.5 py-0.5 text-[10px] leading-none font-semibold tabular-nums"
281
+ >
282
+ {count > 99 ? "99+" : count}
283
+ </span>
284
+ <span className="sr-only">
285
+ {count === 1
286
+ ? "1 conversation needs a human"
287
+ : `${count} conversations need a human`}
288
+ </span>
289
+ </span>
290
+ );
291
+ }
292
+
234
293
  function ActivityGroupList({
235
294
  groups,
236
295
  activeSessionId,
@@ -109,14 +109,69 @@ function baseProps() {
109
109
  };
110
110
  }
111
111
 
112
+ describe("WorkspaceSidebar — Conversations badge (channel-conversations DD-011 D-f)", () => {
113
+ const conversationsRow = (container: HTMLElement) =>
114
+ container.querySelector<HTMLAnchorElement>('[data-row-id="conversations"]')!;
115
+
116
+ it("shows the wants-human count on the Conversations row with a meaning-stating accessible name", () => {
117
+ const { container } = renderSidebar(
118
+ <WorkspaceSidebar {...baseProps()} conversationsBadgeCount={3} />,
119
+ );
120
+
121
+ const row = conversationsRow(container);
122
+ expect(within(row).getByText("3")).toBeDefined();
123
+ expect(within(row).getByText("3 conversations need a human")).toBeDefined();
124
+ });
125
+
126
+ it("speaks the singular for one conversation", () => {
127
+ const { container } = renderSidebar(
128
+ <WorkspaceSidebar {...baseProps()} conversationsBadgeCount={1} />,
129
+ );
130
+
131
+ expect(
132
+ within(conversationsRow(container)).getByText("1 conversation needs a human"),
133
+ ).toBeDefined();
134
+ });
135
+
136
+ it("hides the badge at zero and when the host passes no count", () => {
137
+ const { container } = renderSidebar(
138
+ <WorkspaceSidebar {...baseProps()} conversationsBadgeCount={0} />,
139
+ );
140
+ expect(
141
+ within(conversationsRow(container)).queryByText(/need(s)? a human/),
142
+ ).toBeNull();
143
+
144
+ cleanup();
145
+
146
+ const { container: without } = renderSidebar(
147
+ <WorkspaceSidebar {...baseProps()} />,
148
+ );
149
+ expect(
150
+ within(conversationsRow(without)).queryByText(/need(s)? a human/),
151
+ ).toBeNull();
152
+ });
153
+
154
+ it("caps the visible number at 99+ while the accessible name keeps the real count", () => {
155
+ const { container } = renderSidebar(
156
+ <WorkspaceSidebar {...baseProps()} conversationsBadgeCount={104} />,
157
+ );
158
+
159
+ const row = conversationsRow(container);
160
+ expect(within(row).getByText("99+")).toBeDefined();
161
+ expect(within(row).queryByText("104")).toBeNull();
162
+ expect(within(row).getByText("104 conversations need a human")).toBeDefined();
163
+ });
164
+ });
165
+
112
166
  describe("WorkspaceSidebar — primary navigation", () => {
113
- it("renders the three primary rows through renderLink with stable ids and hrefs", () => {
167
+ it("renders the four primary rows through renderLink with stable ids and hrefs", () => {
114
168
  const { container } = renderSidebar(<WorkspaceSidebar {...baseProps()} />);
115
169
 
116
170
  const row = (id: string) =>
117
171
  container.querySelector<HTMLAnchorElement>(`[data-row-id="${id}"]`);
118
172
  expect(row("new-session")?.getAttribute("href")).toBe("/");
119
173
  expect(row("dashboard")?.getAttribute("href")).toBe("/dashboard");
174
+ expect(row("conversations")?.getAttribute("href")).toBe("/conversations");
120
175
  expect(row("library")?.getAttribute("href")).toBe("/library");
121
176
  });
122
177
 
@@ -34,15 +34,132 @@ describe("AgentCallForm", () => {
34
34
  expect(textarea.value).toBe("Hello world");
35
35
  });
36
36
 
37
- it("renders model input from nested config", () => {
37
+ it("renders model input from the run_config block", () => {
38
38
  const node = makeNode(WorkflowTaskKind.agent_call, {
39
39
  agent: "x",
40
40
  message: "y",
41
- config: { model: "claude-sonnet-4", timeout: 120 },
41
+ run_config: { model_name: "claude-sonnet-4", max_cost_usd: 0.5 },
42
42
  });
43
43
  render(<AgentCallForm node={node} onFieldChange={vi.fn()} />);
44
44
  const input = screen.getByTestId("agent-call-model-input") as HTMLInputElement;
45
45
  expect(input.value).toBe("claude-sonnet-4");
46
+ const budget = screen.getByTestId("agent-call-budget-input") as HTMLInputElement;
47
+ expect(budget.value).toBe("0.5");
48
+ });
49
+
50
+ it("emits an undefined run_config when the last field is cleared", () => {
51
+ const onFieldChange = vi.fn();
52
+ const node = makeNode(WorkflowTaskKind.agent_call, {
53
+ agent: "x",
54
+ message: "y",
55
+ run_config: { max_cost_usd: 0.5 },
56
+ });
57
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
58
+
59
+ const budget = screen.getByTestId("agent-call-budget-input") as HTMLInputElement;
60
+ fireEvent.change(budget, { target: { value: "" } });
61
+
62
+ // Empty means omit: a blank field must not persist a zero override.
63
+ expect(onFieldChange).toHaveBeenCalledWith("run_config", undefined);
64
+ });
65
+
66
+ it("writes budget changes into run_config.max_cost_usd", () => {
67
+ const onFieldChange = vi.fn();
68
+ const node = makeNode(WorkflowTaskKind.agent_call, {
69
+ agent: "x",
70
+ message: "y",
71
+ run_config: { model_name: "claude-sonnet-4" },
72
+ });
73
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
74
+
75
+ const budget = screen.getByTestId("agent-call-budget-input") as HTMLInputElement;
76
+ fireEvent.change(budget, { target: { value: "1.25" } });
77
+
78
+ expect(onFieldChange).toHaveBeenCalledWith("run_config", {
79
+ model_name: "claude-sonnet-4",
80
+ max_cost_usd: 1.25,
81
+ });
82
+ });
83
+
84
+ it("renders workspace entries and edits the git url in place", () => {
85
+ const onFieldChange = vi.fn();
86
+ const node = makeNode(WorkflowTaskKind.agent_call, {
87
+ agent: "x",
88
+ message: "y",
89
+ workspace_entries: [
90
+ { name: "app", source: { git_repo: { url: "https://github.com/acme/app", branch: "main" } } },
91
+ ],
92
+ });
93
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
94
+
95
+ const url = screen.getByTestId("agent-call-workspace-url-0") as HTMLInputElement;
96
+ expect(url.value).toBe("https://github.com/acme/app");
97
+
98
+ fireEvent.change(url, { target: { value: "https://github.com/acme/other" } });
99
+ expect(onFieldChange).toHaveBeenCalledWith("workspace_entries", [
100
+ { name: "app", source: { git_repo: { url: "https://github.com/acme/other", branch: "main" } } },
101
+ ]);
102
+ });
103
+
104
+ it("adds a workspace entry row", () => {
105
+ const onFieldChange = vi.fn();
106
+ const node = makeNode(WorkflowTaskKind.agent_call, { agent: "x", message: "y" });
107
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
108
+
109
+ fireEvent.click(screen.getByTestId("agent-call-workspace-add"));
110
+ expect(onFieldChange).toHaveBeenCalledWith("workspace_entries", [
111
+ { source: { git_repo: { url: "" } } },
112
+ ]);
113
+ });
114
+
115
+ it("emits undefined when the last workspace entry is removed", () => {
116
+ const onFieldChange = vi.fn();
117
+ const node = makeNode(WorkflowTaskKind.agent_call, {
118
+ agent: "x",
119
+ message: "y",
120
+ workspace_entries: [
121
+ { source: { git_repo: { url: "https://github.com/acme/app" } } },
122
+ ],
123
+ });
124
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
125
+
126
+ fireEvent.click(screen.getByLabelText("Remove workspace entry 1"));
127
+ expect(onFieldChange).toHaveBeenCalledWith("workspace_entries", undefined);
128
+ });
129
+
130
+ it("edits environment references as org/slug rows with empty-org omitted", () => {
131
+ const onFieldChange = vi.fn();
132
+ const node = makeNode(WorkflowTaskKind.agent_call, {
133
+ agent: "x",
134
+ message: "y",
135
+ environment_refs: [{ slug: "shared-secrets" }],
136
+ });
137
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
138
+
139
+ const slug = screen.getByTestId("agent-call-envref-slug-0") as HTMLInputElement;
140
+ expect(slug.value).toBe("shared-secrets");
141
+
142
+ fireEvent.change(slug, { target: { value: "other-secrets" } });
143
+ // Empty org stays omitted — an empty string must not become a stored
144
+ // org override (relative refs resolve to the workflow's own org).
145
+ expect(onFieldChange).toHaveBeenCalledWith("environment_refs", [
146
+ { slug: "other-secrets" },
147
+ ]);
148
+ });
149
+
150
+ it("adds and removes environment reference rows", () => {
151
+ const onFieldChange = vi.fn();
152
+ const node = makeNode(WorkflowTaskKind.agent_call, {
153
+ agent: "x",
154
+ message: "y",
155
+ environment_refs: [{ slug: "a" }, { org: "acme", slug: "b" }],
156
+ });
157
+ render(<AgentCallForm node={node} onFieldChange={onFieldChange} />);
158
+
159
+ fireEvent.click(screen.getByLabelText("Remove environment reference 1"));
160
+ expect(onFieldChange).toHaveBeenCalledWith("environment_refs", [
161
+ { org: "acme", slug: "b" },
162
+ ]);
46
163
  });
47
164
 
48
165
  it("shows structured output section when output is present", () => {