@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.
- package/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +21 -2
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts +12 -4
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +18 -24
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/providers.d.ts +23 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +4 -0
- package/channel/providers.js.map +1 -1
- package/channel/useSaveAgentChannel.d.ts +5 -4
- package/channel/useSaveAgentChannel.d.ts.map +1 -1
- package/channel/useSaveAgentChannel.js +18 -4
- package/channel/useSaveAgentChannel.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -2
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationAttentionBanner.d.ts +22 -0
- package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
- package/conversation/ConversationAttentionBanner.js +25 -0
- package/conversation/ConversationAttentionBanner.js.map +1 -0
- package/conversation/ConversationComposer.d.ts +41 -0
- package/conversation/ConversationComposer.d.ts.map +1 -0
- package/conversation/ConversationComposer.js +57 -0
- package/conversation/ConversationComposer.js.map +1 -0
- package/conversation/ConversationControlBanner.d.ts +52 -0
- package/conversation/ConversationControlBanner.d.ts.map +1 -0
- package/conversation/ConversationControlBanner.js +59 -0
- package/conversation/ConversationControlBanner.js.map +1 -0
- package/conversation/ConversationListPane.d.ts +68 -0
- package/conversation/ConversationListPane.d.ts.map +1 -0
- package/conversation/ConversationListPane.js +120 -0
- package/conversation/ConversationListPane.js.map +1 -0
- package/conversation/ConversationTimelineView.d.ts +46 -0
- package/conversation/ConversationTimelineView.d.ts.map +1 -0
- package/conversation/ConversationTimelineView.js +166 -0
- package/conversation/ConversationTimelineView.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts +63 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -0
- package/conversation/ConversationsWorkbench.js +165 -0
- package/conversation/ConversationsWorkbench.js.map +1 -0
- package/conversation/conversationPresentation.d.ts +137 -0
- package/conversation/conversationPresentation.d.ts.map +1 -0
- package/conversation/conversationPresentation.js +230 -0
- package/conversation/conversationPresentation.js.map +1 -0
- package/conversation/index.d.ts +26 -0
- package/conversation/index.d.ts.map +1 -0
- package/conversation/index.js +14 -0
- package/conversation/index.js.map +1 -0
- package/conversation/polling.d.ts +39 -0
- package/conversation/polling.d.ts.map +1 -0
- package/conversation/polling.js +39 -0
- package/conversation/polling.js.map +1 -0
- package/conversation/useConversation.d.ts +61 -0
- package/conversation/useConversation.d.ts.map +1 -0
- package/conversation/useConversation.js +153 -0
- package/conversation/useConversation.js.map +1 -0
- package/conversation/useConversationList.d.ts +98 -0
- package/conversation/useConversationList.d.ts.map +1 -0
- package/conversation/useConversationList.js +200 -0
- package/conversation/useConversationList.js.map +1 -0
- package/conversation/useConversationParticipation.d.ts +63 -0
- package/conversation/useConversationParticipation.d.ts.map +1 -0
- package/conversation/useConversationParticipation.js +84 -0
- package/conversation/useConversationParticipation.js.map +1 -0
- package/conversation/useConversationTimeline.d.ts +71 -0
- package/conversation/useConversationTimeline.d.ts.map +1 -0
- package/conversation/useConversationTimeline.js +152 -0
- package/conversation/useConversationTimeline.js.map +1 -0
- package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
- package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
- package/conversation/useConversationsWantsHumanCount.js +40 -0
- package/conversation/useConversationsWantsHumanCount.js.map +1 -0
- package/execution/useCreateAgentExecution.d.ts +14 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +14 -5
- package/index.js.map +1 -1
- package/internal/Pagination.d.ts +15 -0
- package/internal/Pagination.d.ts.map +1 -0
- package/internal/Pagination.js +20 -0
- package/internal/Pagination.js.map +1 -0
- package/internal/tooltip.d.ts.map +1 -1
- package/internal/tooltip.js +7 -1
- package/internal/tooltip.js.map +1 -1
- package/internal/useAutoScroll.d.ts +25 -4
- package/internal/useAutoScroll.d.ts.map +1 -1
- package/internal/useAutoScroll.js +55 -15
- package/internal/useAutoScroll.js.map +1 -1
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +14 -4
- package/internal/useFetch.js.map +1 -1
- package/models/ModelSelector.d.ts +45 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +49 -6
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +11 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +3 -0
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +27 -0
- package/models/service-tier.d.ts.map +1 -0
- package/models/service-tier.js +34 -0
- package/models/service-tier.js.map +1 -0
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.d.ts +31 -8
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +441 -29
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts +7 -4
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +94 -8
- package/schedule/ScheduleForm.js.map +1 -1
- package/schedule/ScheduleRunsTable.d.ts +72 -0
- package/schedule/ScheduleRunsTable.d.ts.map +1 -0
- package/schedule/ScheduleRunsTable.js +161 -0
- package/schedule/ScheduleRunsTable.js.map +1 -0
- package/schedule/index.d.ts +6 -0
- package/schedule/index.d.ts.map +1 -1
- package/schedule/index.js +3 -0
- package/schedule/index.js.map +1 -1
- package/schedule/useScheduleRuns.d.ts +44 -0
- package/schedule/useScheduleRuns.d.ts.map +1 -0
- package/schedule/useScheduleRuns.js +48 -0
- package/schedule/useScheduleRuns.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +23 -10
- package/schedule/useTriggerSchedule.d.ts.map +1 -1
- package/schedule/useTriggerSchedule.js +28 -9
- package/schedule/useTriggerSchedule.js.map +1 -1
- package/schedule/useUpdateScheduleSpec.d.ts +49 -0
- package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
- package/schedule/useUpdateScheduleSpec.js +70 -0
- package/schedule/useUpdateScheduleSpec.js.map +1 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +1 -0
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +11 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -0
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/sharing/useSaveAgentShare.d.ts +10 -1
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +9 -0
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/sidebar/WorkspaceSidebar.d.ts +12 -4
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
- package/sidebar/WorkspaceSidebar.js +19 -7
- package/sidebar/WorkspaceSidebar.js.map +1 -1
- package/src/billing/CreditLedgerTable.tsx +3 -42
- package/src/channel/AgentChannelsPanel.tsx +33 -2
- package/src/channel/ChannelConversationsDialog.tsx +22 -25
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
- package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
- package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
- package/src/channel/providers.ts +27 -0
- package/src/channel/useSaveAgentChannel.ts +18 -4
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +157 -0
- package/src/conversation/ConversationAttentionBanner.tsx +77 -0
- package/src/conversation/ConversationComposer.tsx +156 -0
- package/src/conversation/ConversationControlBanner.tsx +193 -0
- package/src/conversation/ConversationListPane.tsx +459 -0
- package/src/conversation/ConversationTimelineView.tsx +435 -0
- package/src/conversation/ConversationsWorkbench.tsx +378 -0
- package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
- package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
- package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
- package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
- package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
- package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
- package/src/conversation/__tests__/a11y/harness.tsx +100 -0
- package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
- package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
- package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
- package/src/conversation/__tests__/useConversation.test.tsx +243 -0
- package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
- package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
- package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
- package/src/conversation/conversationPresentation.ts +305 -0
- package/src/conversation/index.ts +80 -0
- package/src/conversation/polling.ts +41 -0
- package/src/conversation/useConversation.ts +221 -0
- package/src/conversation/useConversationList.ts +328 -0
- package/src/conversation/useConversationParticipation.ts +178 -0
- package/src/conversation/useConversationTimeline.ts +244 -0
- package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
- package/src/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +76 -4
- package/src/internal/Pagination.tsx +72 -0
- package/src/internal/__tests__/fake-timer-slices.ts +29 -0
- package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
- package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
- package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
- package/src/internal/tooltip.tsx +6 -1
- package/src/internal/useAutoScroll.ts +66 -20
- package/src/internal/useFetch.ts +15 -4
- package/src/models/ModelSelector.tsx +133 -4
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +256 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +17 -0
- package/src/models/service-tier.ts +49 -0
- package/src/schedule/ScheduleDetailView.tsx +1179 -111
- package/src/schedule/ScheduleForm.tsx +218 -7
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
- package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
- package/src/schedule/index.ts +12 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useTriggerSchedule.ts +39 -14
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
- package/src/sharing/useSaveAgentShare.ts +19 -1
- package/src/sidebar/WorkspaceSidebar.tsx +62 -3
- package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
- package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
- package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
- package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
- package/src/workflow/starter-workflow-yaml.ts +2 -1
- package/styles.css +1 -1
- package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
- package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.js +87 -9
- package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.js +31 -5
- package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
- package/workflow/starter-workflow-yaml.d.ts +1 -1
- package/workflow/starter-workflow-yaml.d.ts.map +1 -1
- package/workflow/starter-workflow-yaml.js +2 -1
- package/workflow/starter-workflow-yaml.js.map +1 -1
|
@@ -10,8 +10,12 @@ import { renderHook, act } from "@testing-library/react";
|
|
|
10
10
|
import { useState, type ReactNode } from "react";
|
|
11
11
|
import { create } from "@bufbuild/protobuf";
|
|
12
12
|
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
13
|
+
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
14
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
13
15
|
import { StigmerContext } from "../../context";
|
|
14
16
|
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
17
|
+
import { ModelRegistryContext } from "../../models/ModelRegistryContext";
|
|
18
|
+
import type { ModelInfo } from "../../models/registry";
|
|
15
19
|
import { useCreateSchedule } from "../useCreateSchedule";
|
|
16
20
|
import { CadenceField } from "../CadenceField";
|
|
17
21
|
import { ScheduleForm } from "../ScheduleForm";
|
|
@@ -294,6 +298,199 @@ describe("ScheduleForm", () => {
|
|
|
294
298
|
});
|
|
295
299
|
});
|
|
296
300
|
|
|
301
|
+
it("submits the budget as runConfig.maxCostUsd, dropping the blank model", async () => {
|
|
302
|
+
const apply = vi.fn().mockResolvedValue(CREATED);
|
|
303
|
+
const client = createClient({ apply });
|
|
304
|
+
const onComplete = vi.fn();
|
|
305
|
+
|
|
306
|
+
render(<ScheduleForm org="isc" onComplete={onComplete} />, {
|
|
307
|
+
wrapper: wrapper(client),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
311
|
+
target: { value: "daily-fee-reminders" },
|
|
312
|
+
});
|
|
313
|
+
fireEvent.change(screen.getByLabelText("Message"), {
|
|
314
|
+
target: { value: "Send today's reminders." },
|
|
315
|
+
});
|
|
316
|
+
await pickAgent();
|
|
317
|
+
fireEvent.change(screen.getByLabelText(/Budget per run/), {
|
|
318
|
+
target: { value: "0.50" },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
|
|
322
|
+
|
|
323
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
|
|
324
|
+
const input = apply.mock.calls[0][0];
|
|
325
|
+
expect(input.agent.runConfig).toEqual({ maxCostUsd: 0.5 });
|
|
326
|
+
// No model picked: neither a model nor a harness is pinned.
|
|
327
|
+
expect(input.agent.harness).toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("submits git workspace entries added through the manual URL input", async () => {
|
|
331
|
+
const apply = vi.fn().mockResolvedValue(CREATED);
|
|
332
|
+
const client = createClient({ apply });
|
|
333
|
+
const onComplete = vi.fn();
|
|
334
|
+
|
|
335
|
+
render(<ScheduleForm org="isc" onComplete={onComplete} />, {
|
|
336
|
+
wrapper: wrapper(client),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
340
|
+
target: { value: "daily-fee-reminders" },
|
|
341
|
+
});
|
|
342
|
+
fireEvent.change(screen.getByLabelText("Message"), {
|
|
343
|
+
target: { value: "Send today's reminders." },
|
|
344
|
+
});
|
|
345
|
+
await pickAgent();
|
|
346
|
+
|
|
347
|
+
// No GitHub connection in this client, so the workspace editor's
|
|
348
|
+
// GitHub action drills into the manual URL panel — no OAuth flow.
|
|
349
|
+
fireEvent.click(screen.getByRole("button", { name: /Connect GitHub/ }));
|
|
350
|
+
fireEvent.change(screen.getByLabelText("Git repository URL"), {
|
|
351
|
+
target: { value: "https://github.com/isc/fee-data.git" },
|
|
352
|
+
});
|
|
353
|
+
fireEvent.change(screen.getByLabelText("Branch (optional)"), {
|
|
354
|
+
target: { value: "main" },
|
|
355
|
+
});
|
|
356
|
+
fireEvent.click(screen.getByRole("button", { name: "Add" }));
|
|
357
|
+
|
|
358
|
+
fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
|
|
359
|
+
|
|
360
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
|
|
361
|
+
const input = apply.mock.calls[0][0];
|
|
362
|
+
expect(input.agent.workspaceEntries).toEqual([
|
|
363
|
+
{
|
|
364
|
+
name: "isc/fee-data",
|
|
365
|
+
source: {
|
|
366
|
+
gitRepo: { url: "https://github.com/isc/fee-data.git", branch: "main" },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("picking a model pins the engine it belongs to (harness + model travel together)", async () => {
|
|
373
|
+
const apply = vi.fn().mockResolvedValue(CREATED);
|
|
374
|
+
const client = createClient({ apply });
|
|
375
|
+
const onComplete = vi.fn();
|
|
376
|
+
|
|
377
|
+
const registryModel: ModelInfo = {
|
|
378
|
+
modelId: "claude-sonnet-4-6",
|
|
379
|
+
provider: "anthropic",
|
|
380
|
+
displayName: "Claude Sonnet 4.6",
|
|
381
|
+
shortDescription: "Balanced default",
|
|
382
|
+
speedTier: "balanced",
|
|
383
|
+
costTier: "standard",
|
|
384
|
+
harness: "cursor",
|
|
385
|
+
featured: true,
|
|
386
|
+
serviceTiers: [],
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
function RegistryWrapper({ children }: { children: ReactNode }) {
|
|
390
|
+
const Base = wrapper(client);
|
|
391
|
+
return (
|
|
392
|
+
<ModelRegistryContext.Provider
|
|
393
|
+
value={{
|
|
394
|
+
models: [registryModel],
|
|
395
|
+
isLoading: false,
|
|
396
|
+
error: null,
|
|
397
|
+
refetch: () => {},
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
<Base>{children}</Base>
|
|
401
|
+
</ModelRegistryContext.Provider>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
render(<ScheduleForm org="isc" onComplete={onComplete} />, {
|
|
406
|
+
wrapper: RegistryWrapper,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
410
|
+
target: { value: "daily-fee-reminders" },
|
|
411
|
+
});
|
|
412
|
+
fireEvent.change(screen.getByLabelText("Message"), {
|
|
413
|
+
target: { value: "Send today's reminders." },
|
|
414
|
+
});
|
|
415
|
+
await pickAgent();
|
|
416
|
+
|
|
417
|
+
// Nothing pinned yet: the trigger shows the placeholder.
|
|
418
|
+
fireEvent.click(screen.getByRole("button", { name: /Platform default/ }));
|
|
419
|
+
fireEvent.click(
|
|
420
|
+
await screen.findByRole("option", { name: /Claude Sonnet 4\.6/ }),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
|
|
424
|
+
|
|
425
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
|
|
426
|
+
const input = apply.mock.calls[0][0];
|
|
427
|
+
expect(input.agent.runConfig).toEqual({ modelName: "claude-sonnet-4-6" });
|
|
428
|
+
expect(input.agent.harness).toBe(Harness.CURSOR);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("toggling the fast tier on a capable model lands service_tier on run_config (#357)", async () => {
|
|
432
|
+
const apply = vi.fn().mockResolvedValue(CREATED);
|
|
433
|
+
const client = createClient({ apply });
|
|
434
|
+
const onComplete = vi.fn();
|
|
435
|
+
|
|
436
|
+
const fastCapable: ModelInfo = {
|
|
437
|
+
modelId: "composer-2.5",
|
|
438
|
+
provider: "cursor",
|
|
439
|
+
displayName: "Composer 2.5",
|
|
440
|
+
shortDescription: "Fast agentic model",
|
|
441
|
+
speedTier: "fastest",
|
|
442
|
+
costTier: "economy",
|
|
443
|
+
harness: "cursor",
|
|
444
|
+
featured: true,
|
|
445
|
+
serviceTiers: ["fast"],
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
function RegistryWrapper({ children }: { children: ReactNode }) {
|
|
449
|
+
const Base = wrapper(client);
|
|
450
|
+
return (
|
|
451
|
+
<ModelRegistryContext.Provider
|
|
452
|
+
value={{
|
|
453
|
+
models: [fastCapable],
|
|
454
|
+
isLoading: false,
|
|
455
|
+
error: null,
|
|
456
|
+
refetch: () => {},
|
|
457
|
+
}}
|
|
458
|
+
>
|
|
459
|
+
<Base>{children}</Base>
|
|
460
|
+
</ModelRegistryContext.Provider>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
render(<ScheduleForm org="isc" onComplete={onComplete} />, {
|
|
465
|
+
wrapper: RegistryWrapper,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
469
|
+
target: { value: "daily-fee-reminders" },
|
|
470
|
+
});
|
|
471
|
+
fireEvent.change(screen.getByLabelText("Message"), {
|
|
472
|
+
target: { value: "Send today's reminders." },
|
|
473
|
+
});
|
|
474
|
+
await pickAgent();
|
|
475
|
+
|
|
476
|
+
fireEvent.click(screen.getByRole("button", { name: /Platform default/ }));
|
|
477
|
+
fireEvent.click(await screen.findByRole("option", { name: /Composer 2\.5/ }));
|
|
478
|
+
|
|
479
|
+
// The popover closed on selection; reopen it — the options-area switch
|
|
480
|
+
// renders only for the now-selected fast-capable model.
|
|
481
|
+
fireEvent.click(screen.getByRole("button", { name: /Composer 2\.5/ }));
|
|
482
|
+
fireEvent.click(await screen.findByRole("switch", { name: "Fast tier" }));
|
|
483
|
+
|
|
484
|
+
fireEvent.click(screen.getByRole("button", { name: "Create schedule" }));
|
|
485
|
+
|
|
486
|
+
await waitFor(() => expect(onComplete).toHaveBeenCalledWith(CREATED));
|
|
487
|
+
const input = apply.mock.calls[0][0];
|
|
488
|
+
expect(input.agent.runConfig).toEqual({
|
|
489
|
+
modelName: "composer-2.5",
|
|
490
|
+
serviceTier: ServiceTier.FAST,
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
297
494
|
it("surfaces the server error verbatim and stays editable", async () => {
|
|
298
495
|
const apply = vi
|
|
299
496
|
.fn()
|
|
@@ -3,7 +3,14 @@ import { renderHook, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { clone, create, equals } from "@bufbuild/protobuf";
|
|
5
5
|
import { ScheduleSchema, type Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ScheduleListSchema,
|
|
8
|
+
ScheduleTriggerResultSchema,
|
|
9
|
+
ScheduleRunListSchema,
|
|
10
|
+
ScheduleRunSchema,
|
|
11
|
+
ScheduleRunOutcome,
|
|
12
|
+
ScheduleRunOrigin,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
|
|
7
14
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
8
15
|
import { StigmerError } from "@stigmer/sdk";
|
|
9
16
|
import { StigmerContext } from "../../context";
|
|
@@ -13,6 +20,7 @@ import { useScheduleList } from "../useScheduleList";
|
|
|
13
20
|
import { useScheduleCount } from "../useScheduleCount";
|
|
14
21
|
import { useResumeSchedule } from "../useResumeSchedule";
|
|
15
22
|
import { useTriggerSchedule } from "../useTriggerSchedule";
|
|
23
|
+
import { useScheduleRuns } from "../useScheduleRuns";
|
|
16
24
|
import { useSetScheduleEnabled } from "../useSetScheduleEnabled";
|
|
17
25
|
|
|
18
26
|
vi.mock("../../feedback/toast", () => ({
|
|
@@ -196,23 +204,56 @@ describe("useResumeSchedule", () => {
|
|
|
196
204
|
});
|
|
197
205
|
|
|
198
206
|
describe("useTriggerSchedule", () => {
|
|
199
|
-
it("
|
|
200
|
-
const
|
|
207
|
+
it("resolves a STARTED result and toasts success", async () => {
|
|
208
|
+
const started = create(ScheduleTriggerResultSchema, {
|
|
209
|
+
outcome: ScheduleRunOutcome.STARTED,
|
|
210
|
+
executionId: "aex_01run",
|
|
211
|
+
schedule: SCHEDULE,
|
|
212
|
+
});
|
|
213
|
+
const trigger = vi.fn().mockResolvedValue(started);
|
|
201
214
|
const client = { schedule: { trigger } };
|
|
202
215
|
|
|
203
216
|
const { result } = renderHook(() => useTriggerSchedule(), {
|
|
204
217
|
wrapper: wrapper(client),
|
|
205
218
|
});
|
|
206
219
|
|
|
207
|
-
await result.current.triggerSchedule("sch_01example");
|
|
220
|
+
const res = await result.current.triggerSchedule("sch_01example");
|
|
208
221
|
|
|
209
222
|
expect(trigger).toHaveBeenCalledWith("sch_01example");
|
|
223
|
+
expect(res.outcome).toBe(ScheduleRunOutcome.STARTED);
|
|
224
|
+
expect(res.executionId).toBe("aex_01run");
|
|
210
225
|
expect(toast.success).toHaveBeenCalledWith("Run started");
|
|
211
226
|
});
|
|
212
227
|
|
|
213
|
-
it("
|
|
214
|
-
//
|
|
215
|
-
//
|
|
228
|
+
it("resolves a REFUSED result and toasts the gate's reason verbatim", async () => {
|
|
229
|
+
// A refused run is a SUCCESSFUL trigger honestly reported (DD-017
|
|
230
|
+
// D-6): the promise resolves, and the gate's own copy is surfaced,
|
|
231
|
+
// never paraphrased.
|
|
232
|
+
const refused = create(ScheduleTriggerResultSchema, {
|
|
233
|
+
outcome: ScheduleRunOutcome.REFUSED,
|
|
234
|
+
refusalReason:
|
|
235
|
+
"run refused: The organization cannot fund a scheduled run right now.",
|
|
236
|
+
schedule: SCHEDULE,
|
|
237
|
+
});
|
|
238
|
+
const trigger = vi.fn().mockResolvedValue(refused);
|
|
239
|
+
const client = { schedule: { trigger } };
|
|
240
|
+
|
|
241
|
+
const { result } = renderHook(() => useTriggerSchedule(), {
|
|
242
|
+
wrapper: wrapper(client),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const res = await result.current.triggerSchedule("sch_01example");
|
|
246
|
+
|
|
247
|
+
expect(res.outcome).toBe(ScheduleRunOutcome.REFUSED);
|
|
248
|
+
expect(toast.error).toHaveBeenCalledWith(
|
|
249
|
+
"run refused: The organization cannot fund a scheduled run right now.",
|
|
250
|
+
);
|
|
251
|
+
expect(toast.success).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("relays a trigger-level gRPC refusal (disabled) verbatim and rejects", async () => {
|
|
255
|
+
// A disabled schedule is refused at the trigger itself — a gRPC
|
|
256
|
+
// error, not a result. Copy is byte-identical across editions.
|
|
216
257
|
const refusal = new StigmerError(
|
|
217
258
|
"failed-precondition",
|
|
218
259
|
"schedule is disabled (spec.enabled=false) — enable it before triggering",
|
|
@@ -231,6 +272,52 @@ describe("useTriggerSchedule", () => {
|
|
|
231
272
|
});
|
|
232
273
|
});
|
|
233
274
|
|
|
275
|
+
describe("useScheduleRuns", () => {
|
|
276
|
+
it("fetches a schedule's run history, newest first", async () => {
|
|
277
|
+
const listRuns = vi.fn().mockResolvedValue(
|
|
278
|
+
create(ScheduleRunListSchema, {
|
|
279
|
+
totalCount: 2,
|
|
280
|
+
items: [
|
|
281
|
+
create(ScheduleRunSchema, {
|
|
282
|
+
scheduleId: "sch_01example",
|
|
283
|
+
origin: ScheduleRunOrigin.MANUAL,
|
|
284
|
+
outcome: ScheduleRunOutcome.STARTED,
|
|
285
|
+
executionId: "aex_01run",
|
|
286
|
+
}),
|
|
287
|
+
create(ScheduleRunSchema, {
|
|
288
|
+
scheduleId: "sch_01example",
|
|
289
|
+
origin: ScheduleRunOrigin.CRON,
|
|
290
|
+
outcome: ScheduleRunOutcome.REFUSED,
|
|
291
|
+
reason: "run refused: missing credential",
|
|
292
|
+
}),
|
|
293
|
+
],
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
const client = { schedule: { listRuns } };
|
|
297
|
+
|
|
298
|
+
const { result } = renderHook(() => useScheduleRuns("sch_01example"), {
|
|
299
|
+
wrapper: wrapper(client),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
303
|
+
expect(result.current.runs).toHaveLength(2);
|
|
304
|
+
expect(result.current.totalCount).toBe(2);
|
|
305
|
+
expect(result.current.runs[1].reason).toBe("run refused: missing credential");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("skips fetching when scheduleId is null", () => {
|
|
309
|
+
const listRuns = vi.fn();
|
|
310
|
+
const client = { schedule: { listRuns } };
|
|
311
|
+
|
|
312
|
+
const { result } = renderHook(() => useScheduleRuns(null), {
|
|
313
|
+
wrapper: wrapper(client),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.current.runs).toEqual([]);
|
|
317
|
+
expect(listRuns).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
234
321
|
describe("useSetScheduleEnabled", () => {
|
|
235
322
|
it("re-applies the full proto with only spec.enabled flipped", async () => {
|
|
236
323
|
const apply = vi.fn(async (doc: { message: unknown }) => ({
|
package/src/schedule/index.ts
CHANGED
|
@@ -7,6 +7,12 @@ export type { ScheduleListClient, SchedulePage } from "./scheduleListFn.js";
|
|
|
7
7
|
export { useSchedule } from "./useSchedule.js";
|
|
8
8
|
export type { UseScheduleReturn } from "./useSchedule.js";
|
|
9
9
|
|
|
10
|
+
export { useScheduleRuns } from "./useScheduleRuns.js";
|
|
11
|
+
export type {
|
|
12
|
+
UseScheduleRunsOptions,
|
|
13
|
+
UseScheduleRunsReturn,
|
|
14
|
+
} from "./useScheduleRuns.js";
|
|
15
|
+
|
|
10
16
|
export { useScheduleList } from "./useScheduleList.js";
|
|
11
17
|
export type {
|
|
12
18
|
UseScheduleListOptions,
|
|
@@ -28,6 +34,9 @@ export type { UseTriggerScheduleReturn } from "./useTriggerSchedule.js";
|
|
|
28
34
|
export { useSetScheduleEnabled } from "./useSetScheduleEnabled.js";
|
|
29
35
|
export type { UseSetScheduleEnabledReturn } from "./useSetScheduleEnabled.js";
|
|
30
36
|
|
|
37
|
+
export { useUpdateScheduleSpec } from "./useUpdateScheduleSpec.js";
|
|
38
|
+
export type { UseUpdateScheduleSpecReturn } from "./useUpdateScheduleSpec.js";
|
|
39
|
+
|
|
31
40
|
export { createScheduleColumns } from "./scheduleColumns.js";
|
|
32
41
|
export type { ScheduleColumnsOptions } from "./scheduleColumns.js";
|
|
33
42
|
|
|
@@ -37,6 +46,9 @@ export type { ScheduleRowActionsProps } from "./ScheduleRowActions.js";
|
|
|
37
46
|
export { ScheduleDetailView, ScheduleIcon } from "./ScheduleDetailView.js";
|
|
38
47
|
export type { ScheduleDetailViewProps } from "./ScheduleDetailView.js";
|
|
39
48
|
|
|
49
|
+
export { ScheduleRunsTable } from "./ScheduleRunsTable.js";
|
|
50
|
+
export type { ScheduleRunsTableProps } from "./ScheduleRunsTable.js";
|
|
51
|
+
|
|
40
52
|
export {
|
|
41
53
|
cadenceToCron,
|
|
42
54
|
cronToCadence,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
ListScheduleRunsRequestSchema,
|
|
6
|
+
type ScheduleRun,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
|
|
8
|
+
import { useStigmer } from "../hooks.js";
|
|
9
|
+
import { useFetch } from "../internal/useFetch.js";
|
|
10
|
+
|
|
11
|
+
/** Options for {@link useScheduleRuns}. */
|
|
12
|
+
export interface UseScheduleRunsOptions {
|
|
13
|
+
/** Maximum runs per page. @default 25 */
|
|
14
|
+
readonly pageSize?: number;
|
|
15
|
+
/** Page number (1-indexed). @default 1 */
|
|
16
|
+
readonly page?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Return value of {@link useScheduleRuns}. */
|
|
20
|
+
export interface UseScheduleRunsReturn {
|
|
21
|
+
/** Recorded fires for the current page, newest first. */
|
|
22
|
+
readonly runs: readonly ScheduleRun[];
|
|
23
|
+
/** Total number of recorded runs for the schedule. */
|
|
24
|
+
readonly totalCount: number;
|
|
25
|
+
/** `true` while the initial fetch is in flight. */
|
|
26
|
+
readonly isLoading: boolean;
|
|
27
|
+
/** `true` while a background refetch is in flight. */
|
|
28
|
+
readonly isRefetching: boolean;
|
|
29
|
+
/** Error from the last failed request, or `null` when healthy. */
|
|
30
|
+
readonly error: Error | null;
|
|
31
|
+
/** Discard cached data and re-fetch the current page from the server. */
|
|
32
|
+
readonly refetch: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RunsPage {
|
|
36
|
+
readonly runs: readonly ScheduleRun[];
|
|
37
|
+
readonly totalCount: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const INITIAL_PAGE: RunsPage = { runs: [], totalCount: 0 };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Data hook that fetches a schedule's run history, newest first — the
|
|
44
|
+
* fire ledger (project DD-017 D-7).
|
|
45
|
+
*
|
|
46
|
+
* Every fire leaves a row, INCLUDING the fires that created no execution
|
|
47
|
+
* (a refused launch gate, a missing target agent), carrying the refusing
|
|
48
|
+
* gate's copy verbatim. This is the surface that finally explains
|
|
49
|
+
* `status.consecutive_failures`: the reason is one row away instead of
|
|
50
|
+
* buried in server logs. Rows for in-flight runs are enriched server-side
|
|
51
|
+
* with the execution's live phase, so outcomes never lie.
|
|
52
|
+
*
|
|
53
|
+
* Pass `null` as `scheduleId` to skip fetching (stable no-op) — the
|
|
54
|
+
* detail view does this while the schedule itself is still loading.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* const { runs, totalCount, isLoading } = useScheduleRuns(schedule?.metadata?.id ?? null);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function useScheduleRuns(
|
|
62
|
+
scheduleId: string | null,
|
|
63
|
+
options?: UseScheduleRunsOptions,
|
|
64
|
+
): UseScheduleRunsReturn {
|
|
65
|
+
const stigmer = useStigmer();
|
|
66
|
+
const pageSize = options?.pageSize ?? 25;
|
|
67
|
+
const page = options?.page ?? 1;
|
|
68
|
+
|
|
69
|
+
const { data, isLoading, isRefetching, error, refetch } = useFetch<RunsPage>(
|
|
70
|
+
scheduleId
|
|
71
|
+
? async () => {
|
|
72
|
+
const result = await stigmer.schedule.listRuns(
|
|
73
|
+
create(ListScheduleRunsRequestSchema, {
|
|
74
|
+
scheduleId,
|
|
75
|
+
pageInfo: { num: page, size: pageSize },
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
return { runs: result.items, totalCount: result.totalCount };
|
|
79
|
+
}
|
|
80
|
+
: null,
|
|
81
|
+
[stigmer, scheduleId, page, pageSize],
|
|
82
|
+
INITIAL_PAGE,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
runs: data.runs,
|
|
87
|
+
totalCount: data.totalCount,
|
|
88
|
+
isLoading,
|
|
89
|
+
isRefetching,
|
|
90
|
+
error,
|
|
91
|
+
refetch,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useMemo, useState } from "react";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
ScheduleRunOutcome,
|
|
6
|
+
type ScheduleTriggerResult,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
|
|
5
8
|
import { getUserMessage } from "@stigmer/sdk";
|
|
6
9
|
import { useStigmer } from "../hooks.js";
|
|
7
10
|
import { toast } from "../feedback/toast.js";
|
|
@@ -11,10 +14,13 @@ import { toError } from "../internal/toError.js";
|
|
|
11
14
|
export interface UseTriggerScheduleReturn {
|
|
12
15
|
/**
|
|
13
16
|
* Fire the schedule once, now — a real agent execution, outside the
|
|
14
|
-
* cron cadence. Resolves with the
|
|
15
|
-
*
|
|
17
|
+
* cron cadence. Resolves with the {@link ScheduleTriggerResult}: the
|
|
18
|
+
* run's REAL outcome (started with an execution id, or refused with the
|
|
19
|
+
* gate's copy), so a caller can navigate to the execution or surface
|
|
20
|
+
* the refusal. Refetch the schedule afterwards to pick up the freshly
|
|
21
|
+
* stamped `status.last_fire_at` / `status.last_execution_id`.
|
|
16
22
|
*/
|
|
17
|
-
readonly triggerSchedule: (scheduleId: string) => Promise<
|
|
23
|
+
readonly triggerSchedule: (scheduleId: string) => Promise<ScheduleTriggerResult>;
|
|
18
24
|
/** `true` while the trigger request is in flight. */
|
|
19
25
|
readonly isTriggering: boolean;
|
|
20
26
|
/** Error from the last failed or refused trigger, or `null` when healthy. */
|
|
@@ -22,13 +28,23 @@ export interface UseTriggerScheduleReturn {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
|
-
* Behavior hook that fires a schedule once, immediately
|
|
31
|
+
* Behavior hook that fires a schedule once, immediately, and reports the
|
|
32
|
+
* run's real outcome (project DD-017 D-5/D-6).
|
|
26
33
|
*
|
|
27
|
-
* The
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
34
|
+
* The trigger is a two-level contract, and this hook honors both levels:
|
|
35
|
+
*
|
|
36
|
+
* - A **gRPC error** means the trigger itself was refused — a disabled
|
|
37
|
+
* schedule answers `FAILED_PRECONDITION` with copy that names the
|
|
38
|
+
* remedy ("enable it before triggering"). The error toast relays that
|
|
39
|
+
* copy verbatim (byte-identical across editions) and the promise
|
|
40
|
+
* rejects.
|
|
41
|
+
* - A **success** means the fire happened. The result's `outcome` then
|
|
42
|
+
* says what the RUN did: `STARTED` (an execution was created — a
|
|
43
|
+
* success toast), or `REFUSED` / `TARGET_MISSING` (a launch gate said
|
|
44
|
+
* no — an error toast carrying the gate's `refusalReason` verbatim).
|
|
45
|
+
* The promise RESOLVES in every one of these cases: a refused run is a
|
|
46
|
+
* successful trigger honestly reported, and the caller decides what to
|
|
47
|
+
* do with it (navigate to the execution, show the reason).
|
|
32
48
|
*
|
|
33
49
|
* Triggering starts a real, billable execution — gate the call behind a
|
|
34
50
|
* confirmation (the detail view uses `ConfirmDialog`).
|
|
@@ -39,13 +55,22 @@ export function useTriggerSchedule(): UseTriggerScheduleReturn {
|
|
|
39
55
|
const [error, setError] = useState<Error | null>(null);
|
|
40
56
|
|
|
41
57
|
const triggerSchedule = useCallback(
|
|
42
|
-
async (scheduleId: string): Promise<
|
|
58
|
+
async (scheduleId: string): Promise<ScheduleTriggerResult> => {
|
|
43
59
|
setIsTriggering(true);
|
|
44
60
|
setError(null);
|
|
45
61
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
const result = await stigmer.schedule.trigger(scheduleId);
|
|
63
|
+
if (result.outcome === ScheduleRunOutcome.STARTED) {
|
|
64
|
+
toast.success("Run started");
|
|
65
|
+
} else {
|
|
66
|
+
// The fire happened but the run was refused — surface the
|
|
67
|
+
// gate's own copy, never a generic message. The promise still
|
|
68
|
+
// resolves; the caller renders the reason inline too.
|
|
69
|
+
toast.error(
|
|
70
|
+
result.refusalReason || "The run was refused by a launch gate",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
49
74
|
} catch (err) {
|
|
50
75
|
const wrapped = toError(err);
|
|
51
76
|
setError(wrapped);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { clone } from "@bufbuild/protobuf";
|
|
5
|
+
import {
|
|
6
|
+
ScheduleSchema,
|
|
7
|
+
type Schedule,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
9
|
+
import type { ScheduleSpec } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/spec_pb";
|
|
10
|
+
import { getUserMessage, manifestDocumentForResource } from "@stigmer/sdk";
|
|
11
|
+
import { useStigmer } from "../hooks.js";
|
|
12
|
+
import { toast } from "../feedback/toast.js";
|
|
13
|
+
|
|
14
|
+
/** Return value of {@link useUpdateScheduleSpec}. */
|
|
15
|
+
export interface UseUpdateScheduleSpecReturn {
|
|
16
|
+
/**
|
|
17
|
+
* Apply one spec mutation to the schedule. Takes the full fetched
|
|
18
|
+
* schedule (not just an id) because the write re-applies the whole
|
|
19
|
+
* resource. Resolves with the post-apply schedule; the caller
|
|
20
|
+
* refetches or replaces its local copy from it.
|
|
21
|
+
*/
|
|
22
|
+
readonly updateSpec: (
|
|
23
|
+
schedule: Schedule,
|
|
24
|
+
mutate: (spec: ScheduleSpec) => void,
|
|
25
|
+
) => Promise<Schedule>;
|
|
26
|
+
/** `true` while the apply is in flight. */
|
|
27
|
+
readonly isUpdating: boolean;
|
|
28
|
+
/** Error from the last failed apply, or `null` when healthy. */
|
|
29
|
+
readonly error: Error | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Behavior hook behind the schedule detail view's inline editing: apply
|
|
34
|
+
* a single spec mutation losslessly.
|
|
35
|
+
*
|
|
36
|
+
* The write path is deliberate, and it is NOT the agent detail view's
|
|
37
|
+
* input-based one: clone the fetched proto, let the caller mutate the
|
|
38
|
+
* spec in place, and re-apply the WHOLE resource through the manifest
|
|
39
|
+
* engine (`stigmer.manifest.apply`) — never down-convert to the curated
|
|
40
|
+
* `ScheduleInput` shape. The server replaces spec and mutable metadata
|
|
41
|
+
* wholesale on update, and `ScheduleInput` cannot express every
|
|
42
|
+
* metadata field (tags, description), so the input route would silently
|
|
43
|
+
* wipe them. The full-proto route is lossless by construction; the
|
|
44
|
+
* server ignores client-provided status, and re-applying never clears a
|
|
45
|
+
* platform pause (Resume is the one clearing path — see
|
|
46
|
+
* `useResumeSchedule`). Same mechanics as {@link useSetScheduleEnabled},
|
|
47
|
+
* generalized from one hardcoded field to any spec edit.
|
|
48
|
+
*
|
|
49
|
+
* Errors are NOT toasted here: inline editors surface the server's
|
|
50
|
+
* message next to the field that was edited (DD-006), and a toast on
|
|
51
|
+
* top would say the same thing twice.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* const { updateSpec, isUpdating } = useUpdateScheduleSpec();
|
|
56
|
+
*
|
|
57
|
+
* await updateSpec(schedule, (spec) => {
|
|
58
|
+
* spec.cron = "0 9 * * 1-5";
|
|
59
|
+
* spec.timeZone = "Asia/Kolkata";
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function useUpdateScheduleSpec(): UseUpdateScheduleSpecReturn {
|
|
64
|
+
const stigmer = useStigmer();
|
|
65
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
66
|
+
const [error, setError] = useState<Error | null>(null);
|
|
67
|
+
|
|
68
|
+
const updateSpec = useCallback(
|
|
69
|
+
async (
|
|
70
|
+
schedule: Schedule,
|
|
71
|
+
mutate: (spec: ScheduleSpec) => void,
|
|
72
|
+
): Promise<Schedule> => {
|
|
73
|
+
if (!schedule.spec) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Cannot edit this schedule: it has no spec. Pass a schedule " +
|
|
76
|
+
"as returned by the server (get/getByReference).",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setIsUpdating(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
const updated = clone(ScheduleSchema, schedule);
|
|
84
|
+
mutate(updated.spec!);
|
|
85
|
+
|
|
86
|
+
const applied = await stigmer.manifest.apply(
|
|
87
|
+
manifestDocumentForResource(updated),
|
|
88
|
+
);
|
|
89
|
+
toast.success("Schedule updated");
|
|
90
|
+
return applied.message as Schedule;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Rethrow with the user-facing message: the inline editors render
|
|
93
|
+
// `message` verbatim next to the edited field.
|
|
94
|
+
const wrapped = new Error(getUserMessage(err));
|
|
95
|
+
setError(wrapped);
|
|
96
|
+
throw wrapped;
|
|
97
|
+
} finally {
|
|
98
|
+
setIsUpdating(false);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[stigmer],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return useMemo(
|
|
105
|
+
() => ({ updateSpec, isUpdating, error }),
|
|
106
|
+
[updateSpec, isUpdating, error],
|
|
107
|
+
);
|
|
108
|
+
}
|