@stigmer/react 3.6.0 → 3.7.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 (121) 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/composer/ComposerToolbar.d.ts +6 -1
  5. package/composer/ComposerToolbar.d.ts.map +1 -1
  6. package/composer/ComposerToolbar.js +2 -2
  7. package/composer/ComposerToolbar.js.map +1 -1
  8. package/composer/SessionComposer.d.ts +9 -0
  9. package/composer/SessionComposer.d.ts.map +1 -1
  10. package/composer/SessionComposer.js +13 -2
  11. package/composer/SessionComposer.js.map +1 -1
  12. package/execution/useCreateAgentExecution.d.ts +14 -0
  13. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  14. package/execution/useCreateAgentExecution.js +5 -0
  15. package/execution/useCreateAgentExecution.js.map +1 -1
  16. package/index.d.ts +2 -2
  17. package/index.d.ts.map +1 -1
  18. package/index.js +6 -5
  19. package/index.js.map +1 -1
  20. package/internal/Pagination.d.ts +15 -0
  21. package/internal/Pagination.d.ts.map +1 -0
  22. package/internal/Pagination.js +20 -0
  23. package/internal/Pagination.js.map +1 -0
  24. package/models/ModelSelector.d.ts +26 -1
  25. package/models/ModelSelector.d.ts.map +1 -1
  26. package/models/ModelSelector.js +27 -6
  27. package/models/ModelSelector.js.map +1 -1
  28. package/models/index.d.ts +2 -0
  29. package/models/index.d.ts.map +1 -1
  30. package/models/index.js +1 -0
  31. package/models/index.js.map +1 -1
  32. package/models/registry.d.ts +10 -0
  33. package/models/registry.d.ts.map +1 -1
  34. package/models/registry.js +3 -0
  35. package/models/registry.js.map +1 -1
  36. package/models/service-tier.d.ts +27 -0
  37. package/models/service-tier.d.ts.map +1 -0
  38. package/models/service-tier.js +34 -0
  39. package/models/service-tier.js.map +1 -0
  40. package/package.json +4 -4
  41. package/schedule/ScheduleDetailView.d.ts +31 -8
  42. package/schedule/ScheduleDetailView.d.ts.map +1 -1
  43. package/schedule/ScheduleDetailView.js +441 -29
  44. package/schedule/ScheduleDetailView.js.map +1 -1
  45. package/schedule/ScheduleForm.d.ts +7 -4
  46. package/schedule/ScheduleForm.d.ts.map +1 -1
  47. package/schedule/ScheduleForm.js +93 -8
  48. package/schedule/ScheduleForm.js.map +1 -1
  49. package/schedule/ScheduleRunsTable.d.ts +72 -0
  50. package/schedule/ScheduleRunsTable.d.ts.map +1 -0
  51. package/schedule/ScheduleRunsTable.js +161 -0
  52. package/schedule/ScheduleRunsTable.js.map +1 -0
  53. package/schedule/index.d.ts +6 -0
  54. package/schedule/index.d.ts.map +1 -1
  55. package/schedule/index.js +3 -0
  56. package/schedule/index.js.map +1 -1
  57. package/schedule/useScheduleRuns.d.ts +44 -0
  58. package/schedule/useScheduleRuns.d.ts.map +1 -0
  59. package/schedule/useScheduleRuns.js +48 -0
  60. package/schedule/useScheduleRuns.js.map +1 -0
  61. package/schedule/useTriggerSchedule.d.ts +23 -10
  62. package/schedule/useTriggerSchedule.d.ts.map +1 -1
  63. package/schedule/useTriggerSchedule.js +28 -9
  64. package/schedule/useTriggerSchedule.js.map +1 -1
  65. package/schedule/useUpdateScheduleSpec.d.ts +49 -0
  66. package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
  67. package/schedule/useUpdateScheduleSpec.js +70 -0
  68. package/schedule/useUpdateScheduleSpec.js.map +1 -0
  69. package/session/useNewSessionFlow.d.ts.map +1 -1
  70. package/session/useNewSessionFlow.js +1 -0
  71. package/session/useNewSessionFlow.js.map +1 -1
  72. package/session/useSessionConversation.d.ts +11 -0
  73. package/session/useSessionConversation.d.ts.map +1 -1
  74. package/session/useSessionConversation.js +1 -0
  75. package/session/useSessionConversation.js.map +1 -1
  76. package/session/useSessionPageFlow.d.ts.map +1 -1
  77. package/session/useSessionPageFlow.js +1 -0
  78. package/session/useSessionPageFlow.js.map +1 -1
  79. package/src/billing/CreditLedgerTable.tsx +3 -42
  80. package/src/composer/ComposerToolbar.tsx +9 -0
  81. package/src/composer/SessionComposer.tsx +24 -1
  82. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
  83. package/src/execution/useCreateAgentExecution.ts +18 -0
  84. package/src/index.ts +9 -4
  85. package/src/internal/Pagination.tsx +72 -0
  86. package/src/models/ModelSelector.tsx +100 -3
  87. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
  88. package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
  89. package/src/models/index.ts +2 -0
  90. package/src/models/registry.ts +16 -0
  91. package/src/models/service-tier.ts +49 -0
  92. package/src/schedule/ScheduleDetailView.tsx +1179 -111
  93. package/src/schedule/ScheduleForm.tsx +217 -7
  94. package/src/schedule/ScheduleRunsTable.tsx +424 -0
  95. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
  96. package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
  97. package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
  98. package/src/schedule/index.ts +12 -0
  99. package/src/schedule/useScheduleRuns.ts +93 -0
  100. package/src/schedule/useTriggerSchedule.ts +39 -14
  101. package/src/schedule/useUpdateScheduleSpec.ts +108 -0
  102. package/src/session/useNewSessionFlow.ts +1 -0
  103. package/src/session/useSessionConversation.ts +12 -0
  104. package/src/session/useSessionPageFlow.ts +1 -0
  105. package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
  106. package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
  107. package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
  108. package/src/workflow/starter-workflow-yaml.ts +2 -1
  109. package/styles.css +1 -1
  110. package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
  111. package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
  112. package/workflow/inspector/forms/AgentCallForm.js +87 -9
  113. package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
  114. package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
  115. package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
  116. package/workflow/inspector/tabs/RuntimeTab.js +31 -5
  117. package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
  118. package/workflow/starter-workflow-yaml.d.ts +1 -1
  119. package/workflow/starter-workflow-yaml.d.ts.map +1 -1
  120. package/workflow/starter-workflow-yaml.js +2 -1
  121. package/workflow/starter-workflow-yaml.js.map +1 -1
@@ -1,10 +1,15 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
+ import { create } from "@bufbuild/protobuf";
5
6
  import { timestampDate } from "@bufbuild/protobuf/wkt";
7
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
8
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
9
+ import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
6
10
  import { formatRelativeTime } from "../activity/format-relative-time.js";
7
11
  import { ErrorMessage } from "../error/ErrorMessage.js";
12
+ import { InlineEditTextarea } from "../inline-edit/InlineEditTextarea.js";
8
13
  import { EditResourceYamlDialog } from "../manifest/EditResourceYamlDialog.js";
9
14
  import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
10
15
  import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell.js";
@@ -12,14 +17,38 @@ import { Section } from "../resource-detail/Section.js";
12
17
  import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
13
18
  import { useCopyResource } from "../resource-detail/useCopyResource.js";
14
19
  import { useDeleteResource } from "../resource-detail/useDeleteResource.js";
20
+ import { useDetailTabs } from "../resource-detail/useDetailTabs.js";
15
21
  import { useExportResource } from "../library/useExportResource.js";
22
+ import { ScheduleRunOutcome } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
23
+ import { RunConfigSchema, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/invocation_pb";
24
+ import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
25
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
+ import { cadenceToCron, cronToCadence, describeCadence, validateCron, } from "./cadence.js";
27
+ import { CadenceField } from "./CadenceField.js";
28
+ import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
29
+ import { ModelSelector } from "../models/ModelSelector.js";
30
+ import { HARNESS_META, fromProtoHarness, toProtoHarness, } from "../models/harness.js";
31
+ import { fromProtoServiceTier, toProtoServiceTier, } from "../models/service-tier.js";
16
32
  import { deriveScheduleState, formatNextFire } from "./scheduleState.js";
33
+ import { ScheduleRunsCompactList, ScheduleRunsTable, } from "./ScheduleRunsTable.js";
34
+ import { TimeZoneField, browserTimeZone } from "./TimeZoneField.js";
17
35
  import { useSchedule } from "./useSchedule.js";
36
+ import { useScheduleRuns } from "./useScheduleRuns.js";
18
37
  import { useResumeSchedule } from "./useResumeSchedule.js";
19
38
  import { useSetScheduleEnabled } from "./useSetScheduleEnabled.js";
20
39
  import { useTriggerSchedule } from "./useTriggerSchedule.js";
40
+ import { useUpdateScheduleSpec } from "./useUpdateScheduleSpec.js";
41
+ const OVERVIEW_TAB_ID = "overview";
42
+ const RUNS_TAB_ID = "runs";
43
+ // The Overview strip shows the newest handful of fires; the Runs tab
44
+ // owns the full paginated history.
45
+ const RECENT_RUNS_OPTIONS = { pageSize: 5 };
46
+ /** invocation.proto pins AgentInvocation.message to 8192 characters. */
47
+ const MESSAGE_MAX_LEN = 8192;
21
48
  /**
22
- * Self-contained detail view for a Schedule (stigmer/stigmer#352).
49
+ * Self-contained detail view for a Schedule (stigmer/stigmer#352),
50
+ * split into an Overview tab (definition + status + recent runs) and a
51
+ * Runs tab (the full paginated fire ledger).
23
52
  *
24
53
  * The view's one non-negotiable is rendering the two stop-levers
25
54
  * distinctly, each with its inline remedy:
@@ -32,22 +61,69 @@ import { useTriggerSchedule } from "./useTriggerSchedule.js";
32
61
  *
33
62
  * Trigger ("Run now") starts a real, billable execution and is gated
34
63
  * behind a confirmation; it is disabled while the schedule cannot fire
35
- * (the banner names the remedy). Edit YAML, Export, and Delete round
36
- * out the action set.
64
+ * (the banner names the remedy). With `editable`, the mutable spec
65
+ * fields edit inline; Edit YAML, Export, and Delete round out the
66
+ * action set.
37
67
  *
38
68
  * Handles loading, error, and not-found states automatically.
39
69
  * Zero Console dependencies — safe for platform builder embedding.
40
70
  * All visual properties flow through `--stgm-*` design tokens.
41
71
  */
42
- export function ScheduleDetailView({ org, slug, onNavigateToAgent, onNavigateToExecution, onDeleted, onResourceLoad, now, className, }) {
72
+ export function ScheduleDetailView({ org, slug, onNavigateToAgent, onNavigateToExecution, onDeleted, onResourceLoad, editable = false, additionalTabs, activeTab, onTabChange, defaultTab, now, className, }) {
43
73
  const { schedule, isLoading, error, refetch } = useSchedule(org, slug);
74
+ // The recent-runs fetch stays mounted regardless of the active tab:
75
+ // it feeds the Runs tab badge (total count), the Overview strip, and
76
+ // post-trigger freshness. The Runs tab's full table owns its own
77
+ // paginated fetch inside ScheduleRunsTable.
78
+ const { runs: recentRuns, totalCount: totalRunCount, isLoading: recentRunsLoading, refetch: refetchRecentRuns, } = useScheduleRuns(schedule?.metadata?.id ?? null, RECENT_RUNS_OPTIONS);
44
79
  const { resumeSchedule, isResuming } = useResumeSchedule();
45
80
  const { triggerSchedule, isTriggering } = useTriggerSchedule();
46
81
  const { setEnabled, isPending: isToggling } = useSetScheduleEnabled();
82
+ const { updateSpec, isUpdating } = useUpdateScheduleSpec();
47
83
  const { deleteResource, isDeleting } = useDeleteResource("schedule", schedule?.metadata?.id ?? null, schedule?.metadata?.name);
48
84
  const { confirmState, confirm, handleConfirm, handleCancel } = useConfirmAction();
49
85
  const { copyId, copyQualifiedSlug } = useCopyResource();
50
86
  const [editOpen, setEditOpen] = useState(false);
87
+ // Remount key for the Runs tab's table: a manual trigger bumps it so
88
+ // the table refetches and returns to page 1, where the new fire
89
+ // appears (the key-remount reset idiom, DD-014).
90
+ const [runsVersion, setRunsVersion] = useState(0);
91
+ // Last failed inline save, attributed to the field that was edited so
92
+ // only that editor shows the message. The backend's message is the
93
+ // UX (DD-006) — e.g. the cron validator's copy surfaces verbatim.
94
+ const [saveError, setSaveError] = useState(null);
95
+ const saveSpecField = useCallback(async (field, mutate) => {
96
+ if (!schedule)
97
+ return false;
98
+ setSaveError(null);
99
+ try {
100
+ await updateSpec(schedule, mutate);
101
+ refetch();
102
+ return true;
103
+ }
104
+ catch (err) {
105
+ setSaveError({
106
+ field,
107
+ message: err instanceof Error ? err.message : String(err),
108
+ });
109
+ return false;
110
+ }
111
+ }, [schedule, updateSpec, refetch]);
112
+ const builtInTabs = useMemo(() => [
113
+ { id: OVERVIEW_TAB_ID, label: "Overview" },
114
+ {
115
+ id: RUNS_TAB_ID,
116
+ label: "Runs",
117
+ ...(totalRunCount > 0 ? { badge: totalRunCount } : {}),
118
+ },
119
+ ], [totalRunCount]);
120
+ const { effectiveTabs, effectiveActiveTab, effectiveOnTabChange, activeAdditionalTab, } = useDetailTabs({
121
+ builtInTabs,
122
+ additionalTabs,
123
+ activeTab,
124
+ onTabChange,
125
+ defaultTab,
126
+ });
51
127
  const { copyYaml, downloadYaml } = useExportResource({
52
128
  kind: "Schedule",
53
129
  resource: schedule,
@@ -69,6 +145,7 @@ export function ScheduleDetailView({ org, slug, onNavigateToAgent, onNavigateToE
69
145
  const stateInfo = deriveScheduleState(spec, status);
70
146
  const renderNow = now ?? new Date();
71
147
  const scheduleId = meta?.id ?? "";
148
+ const scheduleOrg = meta?.org || org;
72
149
  const target = spec?.target?.case === "agent" ? spec.target.value : undefined;
73
150
  const handleResume = async () => {
74
151
  await resumeSchedule(scheduleId);
@@ -78,18 +155,52 @@ export function ScheduleDetailView({ org, slug, onNavigateToAgent, onNavigateToE
78
155
  await setEnabled(schedule, !spec?.enabled);
79
156
  refetch();
80
157
  };
158
+ // One fire: trigger, refresh the schedule and its run history, and — on
159
+ // a started run — hand the execution to the host so it can navigate
160
+ // straight to it (the whole point of the synchronous trigger, DD-017
161
+ // D-6). A refused run resolves too; its reason is toasted by the hook
162
+ // and lands in the run history below.
163
+ const fireNow = async () => {
164
+ const result = await triggerSchedule(scheduleId);
165
+ refetch();
166
+ refetchRecentRuns();
167
+ setRunsVersion((v) => v + 1);
168
+ if (result.outcome === ScheduleRunOutcome.STARTED &&
169
+ result.executionId &&
170
+ onNavigateToExecution) {
171
+ onNavigateToExecution(result.executionId);
172
+ }
173
+ };
81
174
  const handleTrigger = async () => {
82
175
  const confirmed = await confirm({
83
176
  title: "Run this schedule now?",
84
177
  description: "This starts a real agent execution immediately, outside the cron " +
85
- "cadence. The run appears as the schedule's last execution.",
178
+ "cadence. The run is recorded in this schedule's run history.",
86
179
  confirmLabel: "Start run",
87
180
  variant: "default",
88
181
  });
89
182
  if (!confirmed)
90
183
  return;
91
- await triggerSchedule(scheduleId);
92
- refetch();
184
+ await fireNow();
185
+ };
186
+ // A disabled schedule refuses to fire at the server — ScheduleBlueprintAccess
187
+ // requires spec.enabled at the create gate AND the mid-run read (DD-017
188
+ // D-5), so a disabled run would die mid-execution after billing. The
189
+ // staged-disabled test flow the creation form promises is therefore
190
+ // "enable, then fire", and this makes it one click.
191
+ const handleEnableAndRun = async () => {
192
+ const confirmed = await confirm({
193
+ title: "Enable and run this schedule now?",
194
+ description: "This schedule is staged disabled. Enabling it lets it fire on its " +
195
+ "cron cadence going forward, and starts one real run immediately so " +
196
+ "you can see the result. You can disable it again afterwards.",
197
+ confirmLabel: "Enable & run",
198
+ variant: "default",
199
+ });
200
+ if (!confirmed)
201
+ return;
202
+ await setEnabled(schedule, true);
203
+ await fireNow();
93
204
  };
94
205
  const handleDelete = async () => {
95
206
  const confirmed = await confirm({
@@ -104,15 +215,22 @@ export function ScheduleDetailView({ org, slug, onNavigateToAgent, onNavigateToE
104
215
  await deleteResource();
105
216
  onDeleted?.();
106
217
  };
107
- const primaryAction = {
108
- id: "trigger",
109
- label: "Run now",
110
- onAction: () => void handleTrigger(),
111
- // A disabled or paused schedule refuses to trigger server-side; the
112
- // banner above names the remedy, so the button reflects it too
113
- // (error prevention over error recovery).
114
- disabled: stateInfo.state !== "active" || isTriggering,
115
- };
218
+ // Disabled "Enable & run now" (the one-click staged-test flow);
219
+ // active and paused → "Run now" (a paused schedule's owner needs a
220
+ // test fire to verify a fix before resuming — DD-017 D-5).
221
+ const primaryAction = stateInfo.state === "disabled"
222
+ ? {
223
+ id: "enable-and-run",
224
+ label: "Enable & run now",
225
+ onAction: () => void handleEnableAndRun(),
226
+ disabled: isTriggering || isToggling,
227
+ }
228
+ : {
229
+ id: "trigger",
230
+ label: "Run now",
231
+ onAction: () => void handleTrigger(),
232
+ disabled: isTriggering,
233
+ };
116
234
  const actions = [
117
235
  ...(stateInfo.isPaused
118
236
  ? [
@@ -182,18 +300,54 @@ export function ScheduleDetailView({ org, slug, onNavigateToAgent, onNavigateToE
182
300
  status: stateInfo.phase,
183
301
  statusLabel: stateInfo.label,
184
302
  };
185
- const headerBanner = stateInfo.state === "disabled" ? (_jsxs(StateBanner, { title: "Schedule is disabled", onAction: () => void handleToggleEnabled(), actionLabel: "Enable schedule", actionBusy: isToggling, children: ["The owner switch (", _jsx("code", { className: "font-mono", children: "spec.enabled" }), ") is off \u2014 this schedule will not fire.", stateInfo.isPaused && (_jsxs(_Fragment, { children: [" ", "It is also paused by the platform (", status?.pausedReason, "); after enabling, use Resume to clear the pause."] }))] })) : stateInfo.state === "paused" ? (_jsxs(StateBanner, { title: "Paused by the platform", onAction: () => void handleResume(), actionLabel: "Resume schedule", actionBusy: isResuming, children: [status?.pausedReason, " \u2014 resuming clears the pause and the failure streak. Re-applying the manifest does not."] })) : undefined;
186
- return (_jsxs(_Fragment, { children: [_jsx(ResourceDetailShell, { header: headerMeta, headerBanner: headerBanner, primaryAction: primaryAction, actions: actions, className: className, children: _jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(Section, { title: "Definition", children: _jsxs("dl", { className: "divide-y divide-border", children: [_jsx(DetailRow, { label: "Cron", children: _jsx("code", { className: "font-mono text-sm text-foreground", children: spec?.cron || "" }) }), _jsx(DetailRow, { label: "Time zone", children: _jsx("span", { className: "text-sm text-foreground", children: spec?.timeZone || "—" }) }), _jsx(DetailRow, { label: "Target agent", children: target?.agentRef ? (_jsx(ReferenceLink, { label: `${target.agentRef.org}/${target.agentRef.slug}`, onNavigate: onNavigateToAgent
187
- ? () => onNavigateToAgent(target.agentRef.org, target.agentRef.slug)
188
- : undefined })) : (_jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" })) }), _jsx(DetailRow, { label: "Message", children: _jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: target?.message || "—" }) })] }) }), _jsx(Section, { title: "Status", children: _jsxs("dl", { className: "divide-y divide-border", children: [_jsx(DetailRow, { label: "Next fire", children: _jsx("span", { className: "text-sm text-foreground", children: stateInfo.state === "active" && status?.nextFireAt
189
- ? formatNextFire(timestampDate(status.nextFireAt), renderNow)
190
- : "—" }) }), _jsx(DetailRow, { label: "Last fired", children: _jsx("span", { className: "text-sm text-foreground", children: status?.lastFireAt
191
- ? formatRelativeTime(timestampDate(status.lastFireAt), renderNow)
192
- : "Never" }) }), _jsx(DetailRow, { label: "Last execution", children: status?.lastExecutionId ? (_jsx(ReferenceLink, { label: status.lastExecutionId, mono: true, onNavigate: onNavigateToExecution
193
- ? () => onNavigateToExecution(status.lastExecutionId)
194
- : undefined })) : (_jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" })) }), _jsx(DetailRow, { label: "Consecutive failures", children: _jsx("span", { className: cn("text-sm", (status?.consecutiveFailures ?? 0) > 0
195
- ? "font-medium text-warning"
196
- : "text-foreground"), children: status?.consecutiveFailures ?? 0 }) })] }) })] }) }), _jsx(ConfirmDialog, { state: confirmState, onConfirm: handleConfirm, onCancel: handleCancel }), editOpen && (_jsx(EditResourceYamlDialog, { open: editOpen, onOpenChange: setEditOpen, resource: schedule, onApplied: () => refetch() }))] }));
303
+ const headerBanner = stateInfo.state === "disabled" ? (_jsxs(StateBanner, { title: "Schedule is disabled", onAction: () => void handleToggleEnabled(), actionLabel: "Enable schedule", actionBusy: isToggling, children: ["This schedule is staged disabled (", _jsx("code", { className: "font-mono", children: "spec.enabled" }), " is off) \u2014 it will not fire on its cron cadence. Use", " ", _jsx("span", { className: "font-medium text-foreground", children: "Enable & run now" }), " ", "to enable it and start one test run, or Enable it here to hand it to the cadence.", stateInfo.isPaused && (_jsxs(_Fragment, { children: [" ", "It is also paused by the platform (", status?.pausedReason, "); after enabling, use Resume to clear the pause."] }))] })) : stateInfo.state === "paused" ? (_jsxs(StateBanner, { title: "Paused by the platform", onAction: () => void handleResume(), actionLabel: "Resume schedule", actionBusy: isResuming, children: [status?.pausedReason, " \u2014 resuming clears the pause and the failure streak. Re-applying the manifest does not."] })) : undefined;
304
+ const overviewContent = (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(Section, { title: "Definition", children: _jsxs("dl", { className: "divide-y divide-border", children: [_jsx(DetailRow, { label: "Cadence", children: editable ? (_jsx(CadenceInlineEditor, { cron: spec?.cron ?? "", timeZone: spec?.timeZone ?? "", onSave: (cron, timeZone) => saveSpecField("cadence", (s) => {
305
+ s.cron = cron;
306
+ s.timeZone = timeZone;
307
+ }), isSaving: isUpdating, error: saveError?.field === "cadence" ? saveError.message : undefined })) : (_jsx(CadenceSummary, { cron: spec?.cron ?? "", timeZone: spec?.timeZone ?? "" })) }), _jsx(DetailRow, { label: "Target agent", children: target?.agentRef ? (_jsx(ReferenceLink, { label: `${target.agentRef.org}/${target.agentRef.slug}`, onNavigate: onNavigateToAgent
308
+ ? () => onNavigateToAgent(target.agentRef.org, target.agentRef.slug)
309
+ : undefined })) : (_jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" })) }), _jsx(DetailRow, { label: "Message", children: editable && target ? (_jsx(InlineEditTextarea, { value: target.message ?? "", placeholder: "The instruction the agent receives on every fire \u2014 write it for a run with no human present.", onSave: (v) => saveSpecField("message", (s) => {
310
+ if (s.target.case === "agent") {
311
+ s.target.value.message = v.trim();
312
+ }
313
+ }), isSaving: isUpdating, error: saveError?.field === "message" ? saveError.message : undefined, validate: validateMessage })) : (_jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: target?.message || "—" })) }), _jsx(DetailRow, { label: "Environments", children: editable && target ? (_jsx(EnvironmentsInlineEditor, { org: scheduleOrg, refs: target.environmentRefs ?? [], onSave: (refs) => saveSpecField("environments", (s) => {
314
+ if (s.target.case === "agent") {
315
+ s.target.value.environmentRefs = refs.map((r) => create(ApiResourceReferenceSchema, {
316
+ org: r.org,
317
+ slug: r.slug,
318
+ kind: ApiResourceKind.environment,
319
+ }));
320
+ }
321
+ }), isSaving: isUpdating, error: saveError?.field === "environments"
322
+ ? saveError.message
323
+ : undefined })) : (_jsx(EnvironmentRefList, { refs: target?.environmentRefs ?? [] })) }), _jsx(DetailRow, { label: "Workspace", children: _jsx(WorkspaceSummary, { entries: target?.workspaceEntries ?? [] }) }), _jsx(DetailRow, { label: "Engine & model", children: editable && target ? (_jsx(EngineModelInlineEditor, { invocation: target, onSave: (harness, modelName, serviceTier) => saveSpecField("engine-model", (s) => {
324
+ if (s.target.case === "agent") {
325
+ applyEngineModel(s.target.value, harness, modelName, serviceTier);
326
+ }
327
+ }), isSaving: isUpdating, error: saveError?.field === "engine-model"
328
+ ? saveError.message
329
+ : undefined })) : (_jsx(EngineModelSummary, { invocation: target })) }), _jsx(DetailRow, { label: "Budget per run", children: editable && target ? (_jsx(BudgetInlineEditor, { config: target.runConfig, onSave: (maxCostUsd) => saveSpecField("budget", (s) => {
330
+ if (s.target.case === "agent") {
331
+ applyBudget(s.target.value, maxCostUsd);
332
+ }
333
+ }), isSaving: isUpdating, error: saveError?.field === "budget" ? saveError.message : undefined })) : (_jsx(BudgetSummary, { config: target?.runConfig })) })] }) }), _jsx(Section, { title: "Status", children: _jsxs("dl", { className: "divide-y divide-border", children: [_jsx(DetailRow, { label: "Next fire", children: _jsx("span", { className: "text-sm text-foreground", children: stateInfo.state === "active" && status?.nextFireAt
334
+ ? formatNextFire(timestampDate(status.nextFireAt), renderNow)
335
+ : "—" }) }), _jsx(DetailRow, { label: "Last fired", children: _jsx("span", { className: "text-sm text-foreground", children: status?.lastFireAt
336
+ ? formatRelativeTime(timestampDate(status.lastFireAt), renderNow)
337
+ : "Never" }) }), _jsx(DetailRow, { label: "Last execution", children: status?.lastExecutionId ? (_jsx(ReferenceLink, { label: status.lastExecutionId, mono: true, onNavigate: onNavigateToExecution
338
+ ? () => onNavigateToExecution(status.lastExecutionId)
339
+ : undefined })) : (_jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" })) }), _jsx(DetailRow, { label: "Failure streak", children: _jsx(FailureStreak, { count: status?.consecutiveFailures ?? 0 }) })] }) }), _jsx(Section, { title: "Recent runs", headerActions: totalRunCount > recentRuns.length ? (_jsxs("button", { type: "button", onClick: () => effectiveOnTabChange(RUNS_TAB_ID), className: cn("text-xs font-medium text-primary underline-offset-2 hover:underline", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"), children: ["View all ", totalRunCount, " runs"] })) : undefined, children: _jsx(ScheduleRunsCompactList, { runs: recentRuns, isLoading: recentRunsLoading, now: renderNow, onNavigateToExecution: onNavigateToExecution }) })] }));
340
+ let tabContent;
341
+ if (activeAdditionalTab) {
342
+ tabContent = activeAdditionalTab.content;
343
+ }
344
+ else if (effectiveActiveTab === RUNS_TAB_ID) {
345
+ tabContent = (_jsx(ScheduleRunsTable, { scheduleId: scheduleId, now: now, onNavigateToExecution: onNavigateToExecution }, runsVersion));
346
+ }
347
+ else {
348
+ tabContent = overviewContent;
349
+ }
350
+ return (_jsxs(_Fragment, { children: [_jsx(ResourceDetailShell, { header: headerMeta, headerBanner: headerBanner, primaryAction: primaryAction, actions: actions, tabs: effectiveTabs, activeTab: effectiveTabs ? effectiveActiveTab : undefined, onTabChange: effectiveTabs ? effectiveOnTabChange : undefined, tabsAriaLabel: "Schedule detail sections", className: className, children: tabContent }), _jsx(ConfirmDialog, { state: confirmState, onConfirm: handleConfirm, onCancel: handleCancel }), editOpen && (_jsx(EditResourceYamlDialog, { open: editOpen, onOpenChange: setEditOpen, resource: schedule, onApplied: () => refetch() }))] }));
197
351
  }
198
352
  // ---------------------------------------------------------------------------
199
353
  // State banner — one lever, one remedy, inline
@@ -202,11 +356,263 @@ function StateBanner({ title, children, actionLabel, onAction, actionBusy, }) {
202
356
  return (_jsxs("div", { role: "status", className: "flex items-start gap-2.5 rounded-lg border border-warning/30 bg-warning/5 px-4 py-3", children: [_jsx(WarningIcon, { className: "mt-0.5 size-4 shrink-0 text-warning" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: title }), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: children })] }), _jsx("button", { type: "button", onClick: onAction, disabled: actionBusy, className: cn("shrink-0 rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium text-foreground", "hover:bg-accent hover:text-accent-foreground", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-50"), children: actionLabel })] }));
203
357
  }
204
358
  // ---------------------------------------------------------------------------
359
+ // Cadence summary — plain English first, raw cron as the precise record
360
+ // ---------------------------------------------------------------------------
361
+ /**
362
+ * Render a schedule's cadence for reading: the plain-English sentence
363
+ * first ("Every day at 09:00 (Asia/Kolkata)"), the raw cron beneath it
364
+ * in muted mono as the precise record.
365
+ *
366
+ * `cronToCadence` RECOGNIZES rather than parses (the platform owns no
367
+ * cron parser in either edition — see cadence.ts): expressions outside
368
+ * the builder's shapes come back as `custom`, for which the raw cron IS
369
+ * the primary display, with the time zone alongside so nothing the
370
+ * spec stores is hidden.
371
+ */
372
+ function CadenceSummary({ cron, timeZone, }) {
373
+ if (!cron)
374
+ return _jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" });
375
+ const preset = cronToCadence(cron);
376
+ if (preset.kind === "custom") {
377
+ return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("code", { className: "font-mono text-sm text-foreground", children: cron }), timeZone && (_jsx("span", { className: "text-xs text-muted-foreground", children: timeZone }))] }));
378
+ }
379
+ return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-sm text-foreground", children: describeCadence(preset, timeZone || undefined) }), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: cron })] }));
380
+ }
381
+ // ---------------------------------------------------------------------------
382
+ // Inline editors — per-field click-to-edit over the lossless write path
383
+ // ---------------------------------------------------------------------------
384
+ //
385
+ // Each editor follows the InlineEdit* family's contract: read mode is a
386
+ // click target with a hover pencil; edit mode holds a local draft with
387
+ // explicit Save/Cancel; a failed save keeps the editor open with the
388
+ // server's message rendered verbatim beneath it (DD-006). The editors
389
+ // stay in this file (the AgentDetailView single-organism precedent) and
390
+ // reuse the creation form's field components — CadenceField,
391
+ // TimeZoneField, EnvironmentPicker — so creating and editing a schedule
392
+ // are the same experience.
393
+ /** Cadence + time zone edit in one panel — they form one sentence. */
394
+ function CadenceInlineEditor({ cron, timeZone, onSave, isSaving, error, }) {
395
+ const [isEditing, setIsEditing] = useState(false);
396
+ const [draftCadence, setDraftCadence] = useState(() => cronToCadence(cron));
397
+ const [draftZone, setDraftZone] = useState(timeZone);
398
+ const startEdit = () => {
399
+ // cronToCadence round-trips the stored cron into the preset picker;
400
+ // unrecognized shapes land on the Custom escape hatch with the raw
401
+ // string intact.
402
+ setDraftCadence(cronToCadence(cron));
403
+ setDraftZone(timeZone || browserTimeZone());
404
+ setIsEditing(true);
405
+ };
406
+ if (!isEditing) {
407
+ return (_jsx(InlineReadButton, { onEdit: startEdit, ariaLabel: "Edit cadence", children: _jsx(CadenceSummary, { cron: cron, timeZone: timeZone }) }));
408
+ }
409
+ const draftCron = cadenceToCron(draftCadence).trim();
410
+ const canSave = draftCron !== "" && validateCron(draftCron) === null;
411
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(CadenceField, { value: draftCadence, onChange: setDraftCadence, timeZone: draftZone, disabled: isSaving }), _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: editorLabelClasses, children: "Time zone" }), _jsx(TimeZoneField, { value: draftZone, onChange: setDraftZone, disabled: isSaving })] }), _jsx(InlineEditActions, { onCancel: () => setIsEditing(false), onSave: async () => {
412
+ const ok = await onSave(draftCron, draftZone);
413
+ if (ok)
414
+ setIsEditing(false);
415
+ }, isSaving: isSaving, canSave: canSave, error: error })] }));
416
+ }
417
+ /** Environment bindings edit — org-shared credentials only (DD-017 D-2). */
418
+ function EnvironmentsInlineEditor({ org, refs, onSave, isSaving, error, }) {
419
+ const [isEditing, setIsEditing] = useState(false);
420
+ const [draft, setDraft] = useState([]);
421
+ const startEdit = () => {
422
+ setDraft(refs.map((r) => ({ org: r.org, slug: r.slug })));
423
+ setIsEditing(true);
424
+ };
425
+ if (!isEditing) {
426
+ return (_jsx(InlineReadButton, { onEdit: startEdit, ariaLabel: "Edit environments", children: _jsx(EnvironmentRefList, { refs: refs }) }));
427
+ }
428
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(EnvironmentPicker, { org: org, value: draft, onChange: setDraft, disabled: isSaving,
429
+ // Only org-shared environments resolve for a schedule fire (the
430
+ // same credential surface a channel binding uses), so offering
431
+ // anything else could only produce refused runs.
432
+ filterEnvironment: isOrgSharedEnvironment }), _jsx("p", { className: "text-[0.65rem] text-muted-foreground", children: "Bind org-shared credentials so the agent\u2019s tools work on an unattended fire. Without this, an agent whose tools need credentials will be refused every run." }), _jsx(InlineEditActions, { onCancel: () => setIsEditing(false), onSave: async () => {
433
+ const ok = await onSave(draft);
434
+ if (ok)
435
+ setIsEditing(false);
436
+ }, isSaving: isSaving, canSave: true, error: error })] }));
437
+ }
438
+ function isOrgSharedEnvironment(env) {
439
+ return env.metadata?.visibility === ApiResourceVisibility.visibility_org;
440
+ }
441
+ /**
442
+ * Engine & model edit — the composer's own picker, with the creation
443
+ * form's atomic semantics (DD-018 D-5): picking a model pins BOTH the
444
+ * harness and the model (the registry scopes models per harness);
445
+ * clearing the model unpins both, and the platform defaults apply.
446
+ */
447
+ function EngineModelInlineEditor({ invocation, onSave, isSaving, error, }) {
448
+ const [isEditing, setIsEditing] = useState(false);
449
+ const [modelName, setModelName] = useState("");
450
+ const [harness, setHarness] = useState("cursor");
451
+ const [serviceTier, setServiceTier] = useState("standard");
452
+ const startEdit = () => {
453
+ setModelName(invocation.runConfig?.modelName ?? "");
454
+ setHarness(invocation.harness !== Harness.UNSPECIFIED
455
+ ? fromProtoHarness(invocation.harness)
456
+ : "cursor");
457
+ setServiceTier(fromProtoServiceTier(invocation.runConfig?.serviceTier) ?? "standard");
458
+ setIsEditing(true);
459
+ };
460
+ if (!isEditing) {
461
+ return (_jsx(InlineReadButton, { onEdit: startEdit, ariaLabel: "Edit engine and model", children: _jsx(EngineModelSummary, { invocation: invocation }) }));
462
+ }
463
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ModelSelector, { value: modelName, onValueChange: setModelName, initialHarness: harness, onHarnessChange: setHarness, serviceTier: serviceTier, onServiceTierChange: setServiceTier, placeholderLabel: "Platform default", disabled: isSaving }), modelName !== "" && (_jsx("button", { type: "button", onClick: () => {
464
+ setModelName("");
465
+ setServiceTier("standard");
466
+ }, disabled: isSaving, className: "rounded-md px-2 py-1 text-[0.65rem] text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", children: "Reset to platform default" }))] }), _jsx("p", { className: "text-[0.65rem] text-muted-foreground", children: "Runs use the platform\u2019s default engine and model unless you pick one here. Picking a model pins the engine it belongs to." }), _jsx(InlineEditActions, { onCancel: () => setIsEditing(false), onSave: async () => {
467
+ const ok = await onSave(modelName !== "" ? toProtoHarness(harness) : Harness.UNSPECIFIED, modelName, serviceTier);
468
+ if (ok)
469
+ setIsEditing(false);
470
+ }, isSaving: isSaving, canSave: true, error: error })] }));
471
+ }
472
+ /** Budget edit — blank inherits the platform default (DD-018 D-2). */
473
+ function BudgetInlineEditor({ config, onSave, isSaving, error, }) {
474
+ const [isEditing, setIsEditing] = useState(false);
475
+ const [budgetUsd, setBudgetUsd] = useState("");
476
+ const startEdit = () => {
477
+ setBudgetUsd(config && config.maxCostUsd > 0 ? String(config.maxCostUsd) : "");
478
+ setIsEditing(true);
479
+ };
480
+ if (!isEditing) {
481
+ return (_jsx(InlineReadButton, { onEdit: startEdit, ariaLabel: "Edit budget", children: _jsx(BudgetSummary, { config: config }) }));
482
+ }
483
+ const cost = Number.parseFloat(budgetUsd);
484
+ const parsedBudget = Number.isFinite(cost) && cost > 0 ? cost : undefined;
485
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("input", { type: "number", min: "0", step: "any", "aria-label": "Budget per run (USD)", value: budgetUsd, onChange: (e) => setBudgetUsd(e.target.value), placeholder: "platform default", disabled: isSaving, className: cn(editorInputClasses, "sm:max-w-48") }), _jsx("p", { className: "text-[0.65rem] text-muted-foreground", children: "Each run stops when it reaches this spend. You can lower the platform\u2019s per-run cap, never raise it past the platform\u2019s ceiling. Blank inherits the platform default." }), _jsx(InlineEditActions, { onCancel: () => setIsEditing(false), onSave: async () => {
486
+ const ok = await onSave(parsedBudget);
487
+ if (ok)
488
+ setIsEditing(false);
489
+ }, isSaving: isSaving, canSave: true, error: error })] }));
490
+ }
491
+ /**
492
+ * Write the engine+model choice onto the invocation, preserving the
493
+ * run-config fields the editor does not own (budget; the API-only tool
494
+ * rounds), and dropping an all-empty run_config — the proto's "empty =
495
+ * inherit" contract (DD-017 D-3 as carried into DD-018 D-2).
496
+ */
497
+ function applyEngineModel(invocation, harness, modelName, serviceTier) {
498
+ invocation.harness = harness;
499
+ invocation.runConfig = normalizeRunConfig(modelName, invocation.runConfig?.maxCostUsd ?? 0, invocation.runConfig?.maxToolRounds ?? 0,
500
+ // The tier rides the model choice: no model, no tier (#357).
501
+ modelName.trim() !== "" ? serviceTier : "standard");
502
+ }
503
+ /** Budget twin of {@link applyEngineModel} — writes only the cost cap. */
504
+ function applyBudget(invocation, maxCostUsd) {
505
+ invocation.runConfig = normalizeRunConfig(invocation.runConfig?.modelName ?? "", maxCostUsd ?? 0, invocation.runConfig?.maxToolRounds ?? 0, fromProtoServiceTier(invocation.runConfig?.serviceTier) ?? "standard");
506
+ }
507
+ function normalizeRunConfig(modelName, maxCostUsd, maxToolRounds, serviceTier) {
508
+ const fields = {};
509
+ if (modelName.trim() !== "")
510
+ fields.modelName = modelName.trim();
511
+ if (maxCostUsd > 0)
512
+ fields.maxCostUsd = maxCostUsd;
513
+ if (maxToolRounds > 0)
514
+ fields.maxToolRounds = maxToolRounds;
515
+ // Only an active fast choice is carried — an untouched toggle stays
516
+ // absent, preserving the unspecified-vs-explicit ledger distinction (#357).
517
+ if (serviceTier === "fast")
518
+ fields.serviceTier = toProtoServiceTier(serviceTier);
519
+ return Object.keys(fields).length > 0
520
+ ? create(RunConfigSchema, fields)
521
+ : undefined;
522
+ }
523
+ /** Mirrors the server's constraint so the editor rejects bad input instantly. */
524
+ function validateMessage(value) {
525
+ if (!value.trim()) {
526
+ return "Message is required — the agent receives it on every fire.";
527
+ }
528
+ if (value.length > MESSAGE_MAX_LEN) {
529
+ return `Message must be at most ${MESSAGE_MAX_LEN} characters.`;
530
+ }
531
+ return null;
532
+ }
533
+ // ---------------------------------------------------------------------------
534
+ // Inline-edit chrome shared by the bespoke editors above
535
+ // ---------------------------------------------------------------------------
536
+ const editorLabelClasses = "block text-xs font-medium text-foreground";
537
+ const editorInputClasses = cn("w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground", "placeholder:text-muted-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-50");
538
+ /** Read-mode click target with the family's hover pencil. */
539
+ function InlineReadButton({ onEdit, ariaLabel, children, }) {
540
+ return (_jsx("div", { className: "group/inline-edit", children: _jsx("button", { type: "button", onClick: onEdit, "aria-label": ariaLabel, className: cn("-mx-2 w-full rounded-md px-2 py-1.5 text-left transition-colors", "hover:bg-accent-hover cursor-pointer", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"), children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: children }), _jsx(PencilIcon, { className: "mt-0.5 size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/inline-edit:opacity-100" })] }) }) }));
541
+ }
542
+ /** Save/Cancel footer with the field-attributed error line. */
543
+ function InlineEditActions({ onCancel, onSave, isSaving, canSave, error, }) {
544
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [_jsx("button", { type: "button", onClick: onCancel, disabled: isSaving, className: cn("rounded-md px-2.5 py-1 text-xs font-medium", "border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground", "disabled:opacity-50", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), children: "Cancel" }), _jsxs("button", { type: "button", onClick: onSave, disabled: !canSave || isSaving, className: cn("inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium", "bg-primary text-primary-foreground hover:bg-primary-hover", "disabled:pointer-events-none disabled:opacity-50", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), children: [isSaving && _jsx(SpinnerIcon, {}), "Save"] })] }), error && (_jsx("p", { className: "text-xs text-destructive", role: "alert", children: error }))] }));
545
+ }
546
+ // ---------------------------------------------------------------------------
205
547
  // Detail rows and references
206
548
  // ---------------------------------------------------------------------------
207
549
  function DetailRow({ label, children, }) {
208
550
  return (_jsxs("div", { className: "flex items-start gap-4 px-4 py-2.5", children: [_jsx("dt", { className: "w-40 shrink-0 pt-0.5 text-xs font-medium text-muted-foreground", children: label }), _jsx("dd", { className: "min-w-0 flex-1", children: children })] }));
209
551
  }
552
+ /** The bound environment references, or the em-dash when there are none. */
553
+ function EnvironmentRefList({ refs, }) {
554
+ if (refs.length === 0) {
555
+ return _jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" });
556
+ }
557
+ return (_jsx("ul", { className: "flex flex-col gap-0.5", children: refs.map((ref, i) => (_jsx("li", { className: "font-mono text-xs text-foreground", children: ref.org ? `${ref.org}/${ref.slug}` : ref.slug }, `${ref.org}/${ref.slug}-${i}`))) }));
558
+ }
559
+ // ---------------------------------------------------------------------------
560
+ // Failure streak — status.consecutive_failures, explained
561
+ // ---------------------------------------------------------------------------
562
+ /**
563
+ * The platform's failure streak: how many SCHEDULED runs in a row ended
564
+ * badly. The server increments it per failed cron fire, resets it on a
565
+ * successful run (or Resume), and auto-pauses the schedule when the
566
+ * streak crosses its threshold. Manual "Run now" fires never count.
567
+ *
568
+ * The threshold itself is server configuration not exposed through the
569
+ * API, so the copy stays qualitative — a hardcoded "of 5" here could
570
+ * silently drift from what the platform actually enforces.
571
+ */
572
+ function FailureStreak({ count }) {
573
+ if (count === 0) {
574
+ return _jsx("span", { className: "text-sm text-foreground", children: "0" });
575
+ }
576
+ return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsxs("span", { className: "text-sm font-medium text-warning", children: [count, " consecutive failed ", count === 1 ? "run" : "runs"] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Failed scheduled runs raise this streak; too many in a row and the platform pauses the schedule automatically. One successful run resets it to 0. Manual runs never count." })] }));
577
+ }
578
+ // ---------------------------------------------------------------------------
579
+ // The invocation's run shape — engine+model, budget, workspace summaries
580
+ // ---------------------------------------------------------------------------
581
+ /** "Cursor · composer-2.5", either half falling back to the platform default. */
582
+ function EngineModelSummary({ invocation, }) {
583
+ const harness = invocation?.harness ?? Harness.UNSPECIFIED;
584
+ const modelName = invocation?.runConfig?.modelName ?? "";
585
+ if (harness === Harness.UNSPECIFIED && modelName === "") {
586
+ return (_jsx("span", { className: "text-sm text-muted-foreground", children: "Platform default" }));
587
+ }
588
+ const parts = [];
589
+ if (harness !== Harness.UNSPECIFIED) {
590
+ parts.push(HARNESS_META[fromProtoHarness(harness)].label);
591
+ }
592
+ parts.push(modelName !== "" ? modelName : "platform-default model");
593
+ if (invocation?.runConfig?.serviceTier === ServiceTier.FAST) {
594
+ parts.push("Fast tier");
595
+ }
596
+ return _jsx("span", { className: "text-sm text-foreground", children: parts.join(" · ") });
597
+ }
598
+ /** The per-run cost cap, plus the API-only tool-round bound when set. */
599
+ function BudgetSummary({ config, }) {
600
+ const maxCostUsd = config?.maxCostUsd ?? 0;
601
+ const maxToolRounds = config?.maxToolRounds ?? 0;
602
+ return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [maxCostUsd > 0 ? (_jsxs("span", { className: "text-sm text-foreground", children: ["\u2264 $", maxCostUsd.toFixed(2), " per run"] })) : (_jsx("span", { className: "text-sm text-muted-foreground", children: "Platform default" })), maxToolRounds > 0 && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u2264 ", maxToolRounds, " tool rounds"] }))] }));
603
+ }
604
+ /** The git workspace each fire clones, or the em-dash when none. */
605
+ function WorkspaceSummary({ entries, }) {
606
+ if (entries.length === 0) {
607
+ return _jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" });
608
+ }
609
+ return (_jsx("ul", { className: "flex flex-col gap-0.5", children: entries.map((entry, i) => {
610
+ const git = entry.source?.source?.case === "gitRepo"
611
+ ? entry.source.source.value
612
+ : undefined;
613
+ return (_jsxs("li", { className: "text-xs text-foreground", children: [_jsx("span", { className: "font-medium", children: entry.name }), git?.url && (_jsxs("span", { className: "ml-1.5 font-mono text-muted-foreground", children: [git.url, git.branch ? `@${git.branch}` : ""] }))] }, `${entry.name}-${i}`));
614
+ }) }));
615
+ }
210
616
  function ReferenceLink({ label, onNavigate, mono, }) {
211
617
  const textClass = cn("text-sm", mono && "font-mono text-xs");
212
618
  if (!onNavigate) {
@@ -233,4 +639,10 @@ export function ScheduleIcon({ className }) {
233
639
  function WarningIcon({ className }) {
234
640
  return (_jsxs("svg", { className: className, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M8 1.5 15 14H1L8 1.5Z" }), _jsx("path", { d: "M8 6v4" }), _jsx("path", { d: "M8 12.2v.05" })] }));
235
641
  }
642
+ function PencilIcon({ className }) {
643
+ return (_jsx("svg", { className: className, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M11.5 1.5a2.121 2.121 0 0 1 3 3L5 14l-4 1 1-4Z" }) }));
644
+ }
645
+ function SpinnerIcon() {
646
+ return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", className: "animate-spin", "aria-hidden": "true", children: _jsx("path", { d: "M8 2a6 6 0 1 0 6 6" }) }));
647
+ }
236
648
  //# sourceMappingURL=ScheduleDetailView.js.map