@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.
- package/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.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/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 +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -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/models/ModelSelector.d.ts +26 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +27 -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 +10 -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 +93 -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/src/billing/CreditLedgerTable.tsx +3 -42
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
- package/src/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +9 -4
- package/src/internal/Pagination.tsx +72 -0
- package/src/models/ModelSelector.tsx +100 -3
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +16 -0
- package/src/models/service-tier.ts +49 -0
- package/src/schedule/ScheduleDetailView.tsx +1179 -111
- package/src/schedule/ScheduleForm.tsx +217 -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/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
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
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";
|
|
6
7
|
import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
8
|
+
import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
9
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
10
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
11
|
+
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
12
|
+
import type { ResourceRef } from "@stigmer/sdk";
|
|
7
13
|
import { formatRelativeTime } from "../activity/format-relative-time.js";
|
|
8
14
|
import { ErrorMessage } from "../error/ErrorMessage.js";
|
|
15
|
+
import { InlineEditTextarea } from "../inline-edit/InlineEditTextarea.js";
|
|
9
16
|
import { EditResourceYamlDialog } from "../manifest/EditResourceYamlDialog.js";
|
|
10
17
|
import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
|
|
11
18
|
import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell.js";
|
|
@@ -13,13 +20,57 @@ import { Section } from "../resource-detail/Section.js";
|
|
|
13
20
|
import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
|
|
14
21
|
import { useCopyResource } from "../resource-detail/useCopyResource.js";
|
|
15
22
|
import { useDeleteResource } from "../resource-detail/useDeleteResource.js";
|
|
16
|
-
import
|
|
23
|
+
import { useDetailTabs } from "../resource-detail/useDetailTabs.js";
|
|
24
|
+
import type {
|
|
25
|
+
AdditionalTab,
|
|
26
|
+
DetailAction,
|
|
27
|
+
ResourceHeaderMeta,
|
|
28
|
+
} from "../resource-detail/types.js";
|
|
29
|
+
import type { TabItem } from "../tabs/Tabs.js";
|
|
17
30
|
import { useExportResource } from "../library/useExportResource.js";
|
|
31
|
+
import { ScheduleRunOutcome } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
|
|
32
|
+
import type { ScheduleSpec } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/spec_pb";
|
|
33
|
+
import {
|
|
34
|
+
RunConfigSchema,
|
|
35
|
+
type AgentInvocation,
|
|
36
|
+
type RunConfig,
|
|
37
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/invocation_pb";
|
|
38
|
+
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
39
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
40
|
+
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
41
|
+
import {
|
|
42
|
+
cadenceToCron,
|
|
43
|
+
cronToCadence,
|
|
44
|
+
describeCadence,
|
|
45
|
+
validateCron,
|
|
46
|
+
type CadencePreset,
|
|
47
|
+
} from "./cadence.js";
|
|
48
|
+
import { CadenceField } from "./CadenceField.js";
|
|
49
|
+
import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
|
|
50
|
+
import { ModelSelector } from "../models/ModelSelector.js";
|
|
51
|
+
import {
|
|
52
|
+
HARNESS_META,
|
|
53
|
+
fromProtoHarness,
|
|
54
|
+
toProtoHarness,
|
|
55
|
+
type HarnessOption,
|
|
56
|
+
} from "../models/harness.js";
|
|
57
|
+
import {
|
|
58
|
+
fromProtoServiceTier,
|
|
59
|
+
toProtoServiceTier,
|
|
60
|
+
type ServiceTierOption,
|
|
61
|
+
} from "../models/service-tier.js";
|
|
18
62
|
import { deriveScheduleState, formatNextFire } from "./scheduleState.js";
|
|
63
|
+
import {
|
|
64
|
+
ScheduleRunsCompactList,
|
|
65
|
+
ScheduleRunsTable,
|
|
66
|
+
} from "./ScheduleRunsTable.js";
|
|
67
|
+
import { TimeZoneField, browserTimeZone } from "./TimeZoneField.js";
|
|
19
68
|
import { useSchedule } from "./useSchedule.js";
|
|
69
|
+
import { useScheduleRuns } from "./useScheduleRuns.js";
|
|
20
70
|
import { useResumeSchedule } from "./useResumeSchedule.js";
|
|
21
71
|
import { useSetScheduleEnabled } from "./useSetScheduleEnabled.js";
|
|
22
72
|
import { useTriggerSchedule } from "./useTriggerSchedule.js";
|
|
73
|
+
import { useUpdateScheduleSpec } from "./useUpdateScheduleSpec.js";
|
|
23
74
|
|
|
24
75
|
/** Props for {@link ScheduleDetailView}. */
|
|
25
76
|
export interface ScheduleDetailViewProps {
|
|
@@ -34,8 +85,9 @@ export interface ScheduleDetailViewProps {
|
|
|
34
85
|
*/
|
|
35
86
|
readonly onNavigateToAgent?: (org: string, slug: string) => void;
|
|
36
87
|
/**
|
|
37
|
-
* Called when the user activates
|
|
38
|
-
*
|
|
88
|
+
* Called when the user activates an execution reference (`aex_…`),
|
|
89
|
+
* from the status row or a run-history row. When omitted, ids render
|
|
90
|
+
* as plain text.
|
|
39
91
|
*/
|
|
40
92
|
readonly onNavigateToExecution?: (executionId: string) => void;
|
|
41
93
|
/** Called after a successful delete (navigate back to the list). */
|
|
@@ -43,8 +95,26 @@ export interface ScheduleDetailViewProps {
|
|
|
43
95
|
/** Called when the schedule loads or reloads (e.g. breadcrumb label sync). */
|
|
44
96
|
readonly onResourceLoad?: (schedule: Schedule) => void;
|
|
45
97
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
98
|
+
* Enable inline editing of the schedule's mutable spec fields
|
|
99
|
+
* (cadence, message, environments, engine & model, budget). Each
|
|
100
|
+
* field saves independently via a lossless full-resource re-apply
|
|
101
|
+
* ({@link useUpdateScheduleSpec}). The server-immutable fields —
|
|
102
|
+
* slug, target agent, target type — never gain an edit affordance;
|
|
103
|
+
* workspace edits go through Edit YAML for now.
|
|
104
|
+
* @default false
|
|
105
|
+
*/
|
|
106
|
+
readonly editable?: boolean;
|
|
107
|
+
/** Consumer-provided extension tabs, appended after the built-ins. */
|
|
108
|
+
readonly additionalTabs?: readonly AdditionalTab[];
|
|
109
|
+
/** Controlled active tab (pair with `onTabChange`). */
|
|
110
|
+
readonly activeTab?: string;
|
|
111
|
+
/** Controlled tab change handler (pair with `activeTab`). */
|
|
112
|
+
readonly onTabChange?: (tabId: string) => void;
|
|
113
|
+
/** Initial tab in uncontrolled mode. @default "overview" */
|
|
114
|
+
readonly defaultTab?: string;
|
|
115
|
+
/**
|
|
116
|
+
* The instant "next fire" countdowns and run timestamps are computed
|
|
117
|
+
* against. Injectable for deterministic tests and Scenar fixtures.
|
|
48
118
|
* @default new Date() at render
|
|
49
119
|
*/
|
|
50
120
|
readonly now?: Date;
|
|
@@ -52,8 +122,20 @@ export interface ScheduleDetailViewProps {
|
|
|
52
122
|
readonly className?: string;
|
|
53
123
|
}
|
|
54
124
|
|
|
125
|
+
const OVERVIEW_TAB_ID = "overview";
|
|
126
|
+
const RUNS_TAB_ID = "runs";
|
|
127
|
+
|
|
128
|
+
// The Overview strip shows the newest handful of fires; the Runs tab
|
|
129
|
+
// owns the full paginated history.
|
|
130
|
+
const RECENT_RUNS_OPTIONS = { pageSize: 5 } as const;
|
|
131
|
+
|
|
132
|
+
/** invocation.proto pins AgentInvocation.message to 8192 characters. */
|
|
133
|
+
const MESSAGE_MAX_LEN = 8192;
|
|
134
|
+
|
|
55
135
|
/**
|
|
56
|
-
* Self-contained detail view for a Schedule (stigmer/stigmer#352)
|
|
136
|
+
* Self-contained detail view for a Schedule (stigmer/stigmer#352),
|
|
137
|
+
* split into an Overview tab (definition + status + recent runs) and a
|
|
138
|
+
* Runs tab (the full paginated fire ledger).
|
|
57
139
|
*
|
|
58
140
|
* The view's one non-negotiable is rendering the two stop-levers
|
|
59
141
|
* distinctly, each with its inline remedy:
|
|
@@ -66,8 +148,9 @@ export interface ScheduleDetailViewProps {
|
|
|
66
148
|
*
|
|
67
149
|
* Trigger ("Run now") starts a real, billable execution and is gated
|
|
68
150
|
* behind a confirmation; it is disabled while the schedule cannot fire
|
|
69
|
-
* (the banner names the remedy).
|
|
70
|
-
* out the
|
|
151
|
+
* (the banner names the remedy). With `editable`, the mutable spec
|
|
152
|
+
* fields edit inline; Edit YAML, Export, and Delete round out the
|
|
153
|
+
* action set.
|
|
71
154
|
*
|
|
72
155
|
* Handles loading, error, and not-found states automatically.
|
|
73
156
|
* Zero Console dependencies — safe for platform builder embedding.
|
|
@@ -80,14 +163,31 @@ export function ScheduleDetailView({
|
|
|
80
163
|
onNavigateToExecution,
|
|
81
164
|
onDeleted,
|
|
82
165
|
onResourceLoad,
|
|
166
|
+
editable = false,
|
|
167
|
+
additionalTabs,
|
|
168
|
+
activeTab,
|
|
169
|
+
onTabChange,
|
|
170
|
+
defaultTab,
|
|
83
171
|
now,
|
|
84
172
|
className,
|
|
85
173
|
}: ScheduleDetailViewProps) {
|
|
86
174
|
const { schedule, isLoading, error, refetch } = useSchedule(org, slug);
|
|
87
175
|
|
|
176
|
+
// The recent-runs fetch stays mounted regardless of the active tab:
|
|
177
|
+
// it feeds the Runs tab badge (total count), the Overview strip, and
|
|
178
|
+
// post-trigger freshness. The Runs tab's full table owns its own
|
|
179
|
+
// paginated fetch inside ScheduleRunsTable.
|
|
180
|
+
const {
|
|
181
|
+
runs: recentRuns,
|
|
182
|
+
totalCount: totalRunCount,
|
|
183
|
+
isLoading: recentRunsLoading,
|
|
184
|
+
refetch: refetchRecentRuns,
|
|
185
|
+
} = useScheduleRuns(schedule?.metadata?.id ?? null, RECENT_RUNS_OPTIONS);
|
|
186
|
+
|
|
88
187
|
const { resumeSchedule, isResuming } = useResumeSchedule();
|
|
89
188
|
const { triggerSchedule, isTriggering } = useTriggerSchedule();
|
|
90
189
|
const { setEnabled, isPending: isToggling } = useSetScheduleEnabled();
|
|
190
|
+
const { updateSpec, isUpdating } = useUpdateScheduleSpec();
|
|
91
191
|
const { deleteResource, isDeleting } = useDeleteResource(
|
|
92
192
|
"schedule",
|
|
93
193
|
schedule?.metadata?.id ?? null,
|
|
@@ -98,6 +198,66 @@ export function ScheduleDetailView({
|
|
|
98
198
|
const { copyId, copyQualifiedSlug } = useCopyResource();
|
|
99
199
|
const [editOpen, setEditOpen] = useState(false);
|
|
100
200
|
|
|
201
|
+
// Remount key for the Runs tab's table: a manual trigger bumps it so
|
|
202
|
+
// the table refetches and returns to page 1, where the new fire
|
|
203
|
+
// appears (the key-remount reset idiom, DD-014).
|
|
204
|
+
const [runsVersion, setRunsVersion] = useState(0);
|
|
205
|
+
|
|
206
|
+
// Last failed inline save, attributed to the field that was edited so
|
|
207
|
+
// only that editor shows the message. The backend's message is the
|
|
208
|
+
// UX (DD-006) — e.g. the cron validator's copy surfaces verbatim.
|
|
209
|
+
const [saveError, setSaveError] = useState<{
|
|
210
|
+
field: string;
|
|
211
|
+
message: string;
|
|
212
|
+
} | null>(null);
|
|
213
|
+
|
|
214
|
+
const saveSpecField = useCallback(
|
|
215
|
+
async (
|
|
216
|
+
field: string,
|
|
217
|
+
mutate: (spec: ScheduleSpec) => void,
|
|
218
|
+
): Promise<boolean> => {
|
|
219
|
+
if (!schedule) return false;
|
|
220
|
+
setSaveError(null);
|
|
221
|
+
try {
|
|
222
|
+
await updateSpec(schedule, mutate);
|
|
223
|
+
refetch();
|
|
224
|
+
return true;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
setSaveError({
|
|
227
|
+
field,
|
|
228
|
+
message: err instanceof Error ? err.message : String(err),
|
|
229
|
+
});
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[schedule, updateSpec, refetch],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const builtInTabs = useMemo<readonly TabItem[]>(
|
|
237
|
+
() => [
|
|
238
|
+
{ id: OVERVIEW_TAB_ID, label: "Overview" },
|
|
239
|
+
{
|
|
240
|
+
id: RUNS_TAB_ID,
|
|
241
|
+
label: "Runs",
|
|
242
|
+
...(totalRunCount > 0 ? { badge: totalRunCount } : {}),
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
[totalRunCount],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const {
|
|
249
|
+
effectiveTabs,
|
|
250
|
+
effectiveActiveTab,
|
|
251
|
+
effectiveOnTabChange,
|
|
252
|
+
activeAdditionalTab,
|
|
253
|
+
} = useDetailTabs({
|
|
254
|
+
builtInTabs,
|
|
255
|
+
additionalTabs,
|
|
256
|
+
activeTab,
|
|
257
|
+
onTabChange,
|
|
258
|
+
defaultTab,
|
|
259
|
+
});
|
|
260
|
+
|
|
101
261
|
const { copyYaml, downloadYaml } = useExportResource({
|
|
102
262
|
kind: "Schedule",
|
|
103
263
|
resource: schedule,
|
|
@@ -120,6 +280,7 @@ export function ScheduleDetailView({
|
|
|
120
280
|
const renderNow = now ?? new Date();
|
|
121
281
|
|
|
122
282
|
const scheduleId = meta?.id ?? "";
|
|
283
|
+
const scheduleOrg = meta?.org || org;
|
|
123
284
|
const target = spec?.target?.case === "agent" ? spec.target.value : undefined;
|
|
124
285
|
|
|
125
286
|
const handleResume = async () => {
|
|
@@ -132,18 +293,56 @@ export function ScheduleDetailView({
|
|
|
132
293
|
refetch();
|
|
133
294
|
};
|
|
134
295
|
|
|
296
|
+
// One fire: trigger, refresh the schedule and its run history, and — on
|
|
297
|
+
// a started run — hand the execution to the host so it can navigate
|
|
298
|
+
// straight to it (the whole point of the synchronous trigger, DD-017
|
|
299
|
+
// D-6). A refused run resolves too; its reason is toasted by the hook
|
|
300
|
+
// and lands in the run history below.
|
|
301
|
+
const fireNow = async () => {
|
|
302
|
+
const result = await triggerSchedule(scheduleId);
|
|
303
|
+
refetch();
|
|
304
|
+
refetchRecentRuns();
|
|
305
|
+
setRunsVersion((v) => v + 1);
|
|
306
|
+
if (
|
|
307
|
+
result.outcome === ScheduleRunOutcome.STARTED &&
|
|
308
|
+
result.executionId &&
|
|
309
|
+
onNavigateToExecution
|
|
310
|
+
) {
|
|
311
|
+
onNavigateToExecution(result.executionId);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
135
315
|
const handleTrigger = async () => {
|
|
136
316
|
const confirmed = await confirm({
|
|
137
317
|
title: "Run this schedule now?",
|
|
138
318
|
description:
|
|
139
319
|
"This starts a real agent execution immediately, outside the cron " +
|
|
140
|
-
"cadence. The run
|
|
320
|
+
"cadence. The run is recorded in this schedule's run history.",
|
|
141
321
|
confirmLabel: "Start run",
|
|
142
322
|
variant: "default",
|
|
143
323
|
});
|
|
144
324
|
if (!confirmed) return;
|
|
145
|
-
await
|
|
146
|
-
|
|
325
|
+
await fireNow();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// A disabled schedule refuses to fire at the server — ScheduleBlueprintAccess
|
|
329
|
+
// requires spec.enabled at the create gate AND the mid-run read (DD-017
|
|
330
|
+
// D-5), so a disabled run would die mid-execution after billing. The
|
|
331
|
+
// staged-disabled test flow the creation form promises is therefore
|
|
332
|
+
// "enable, then fire", and this makes it one click.
|
|
333
|
+
const handleEnableAndRun = async () => {
|
|
334
|
+
const confirmed = await confirm({
|
|
335
|
+
title: "Enable and run this schedule now?",
|
|
336
|
+
description:
|
|
337
|
+
"This schedule is staged disabled. Enabling it lets it fire on its " +
|
|
338
|
+
"cron cadence going forward, and starts one real run immediately so " +
|
|
339
|
+
"you can see the result. You can disable it again afterwards.",
|
|
340
|
+
confirmLabel: "Enable & run",
|
|
341
|
+
variant: "default",
|
|
342
|
+
});
|
|
343
|
+
if (!confirmed) return;
|
|
344
|
+
await setEnabled(schedule, true);
|
|
345
|
+
await fireNow();
|
|
147
346
|
};
|
|
148
347
|
|
|
149
348
|
const handleDelete = async () => {
|
|
@@ -160,15 +359,23 @@ export function ScheduleDetailView({
|
|
|
160
359
|
onDeleted?.();
|
|
161
360
|
};
|
|
162
361
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
362
|
+
// Disabled → "Enable & run now" (the one-click staged-test flow);
|
|
363
|
+
// active and paused → "Run now" (a paused schedule's owner needs a
|
|
364
|
+
// test fire to verify a fix before resuming — DD-017 D-5).
|
|
365
|
+
const primaryAction: DetailAction =
|
|
366
|
+
stateInfo.state === "disabled"
|
|
367
|
+
? {
|
|
368
|
+
id: "enable-and-run",
|
|
369
|
+
label: "Enable & run now",
|
|
370
|
+
onAction: () => void handleEnableAndRun(),
|
|
371
|
+
disabled: isTriggering || isToggling,
|
|
372
|
+
}
|
|
373
|
+
: {
|
|
374
|
+
id: "trigger",
|
|
375
|
+
label: "Run now",
|
|
376
|
+
onAction: () => void handleTrigger(),
|
|
377
|
+
disabled: isTriggering,
|
|
378
|
+
};
|
|
172
379
|
|
|
173
380
|
const actions: DetailAction[] = [
|
|
174
381
|
...(stateInfo.isPaused
|
|
@@ -249,8 +456,12 @@ export function ScheduleDetailView({
|
|
|
249
456
|
actionLabel="Enable schedule"
|
|
250
457
|
actionBusy={isToggling}
|
|
251
458
|
>
|
|
252
|
-
|
|
253
|
-
off —
|
|
459
|
+
This schedule is staged disabled (
|
|
460
|
+
<code className="font-mono">spec.enabled</code> is off) — it will not
|
|
461
|
+
fire on its cron cadence. Use{" "}
|
|
462
|
+
<span className="font-medium text-foreground">Enable & run now</span>{" "}
|
|
463
|
+
to enable it and start one test run, or Enable it here to hand it to
|
|
464
|
+
the cadence.
|
|
254
465
|
{stateInfo.isPaused && (
|
|
255
466
|
<>
|
|
256
467
|
{" "}
|
|
@@ -271,6 +482,245 @@ export function ScheduleDetailView({
|
|
|
271
482
|
</StateBanner>
|
|
272
483
|
) : undefined;
|
|
273
484
|
|
|
485
|
+
const overviewContent = (
|
|
486
|
+
<div className="flex flex-col gap-6">
|
|
487
|
+
<Section title="Definition">
|
|
488
|
+
<dl className="divide-y divide-border">
|
|
489
|
+
<DetailRow label="Cadence">
|
|
490
|
+
{editable ? (
|
|
491
|
+
<CadenceInlineEditor
|
|
492
|
+
cron={spec?.cron ?? ""}
|
|
493
|
+
timeZone={spec?.timeZone ?? ""}
|
|
494
|
+
onSave={(cron, timeZone) =>
|
|
495
|
+
saveSpecField("cadence", (s) => {
|
|
496
|
+
s.cron = cron;
|
|
497
|
+
s.timeZone = timeZone;
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
isSaving={isUpdating}
|
|
501
|
+
error={
|
|
502
|
+
saveError?.field === "cadence" ? saveError.message : undefined
|
|
503
|
+
}
|
|
504
|
+
/>
|
|
505
|
+
) : (
|
|
506
|
+
<CadenceSummary
|
|
507
|
+
cron={spec?.cron ?? ""}
|
|
508
|
+
timeZone={spec?.timeZone ?? ""}
|
|
509
|
+
/>
|
|
510
|
+
)}
|
|
511
|
+
</DetailRow>
|
|
512
|
+
{/* Target agent and target type are server-immutable on update,
|
|
513
|
+
so this row never gains an edit affordance — error
|
|
514
|
+
prevention over error handling. */}
|
|
515
|
+
<DetailRow label="Target agent">
|
|
516
|
+
{target?.agentRef ? (
|
|
517
|
+
<ReferenceLink
|
|
518
|
+
label={`${target.agentRef.org}/${target.agentRef.slug}`}
|
|
519
|
+
onNavigate={
|
|
520
|
+
onNavigateToAgent
|
|
521
|
+
? () =>
|
|
522
|
+
onNavigateToAgent(
|
|
523
|
+
target.agentRef!.org,
|
|
524
|
+
target.agentRef!.slug,
|
|
525
|
+
)
|
|
526
|
+
: undefined
|
|
527
|
+
}
|
|
528
|
+
/>
|
|
529
|
+
) : (
|
|
530
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
531
|
+
)}
|
|
532
|
+
</DetailRow>
|
|
533
|
+
<DetailRow label="Message">
|
|
534
|
+
{editable && target ? (
|
|
535
|
+
<InlineEditTextarea
|
|
536
|
+
value={target.message ?? ""}
|
|
537
|
+
placeholder="The instruction the agent receives on every fire — write it for a run with no human present."
|
|
538
|
+
onSave={(v) =>
|
|
539
|
+
saveSpecField("message", (s) => {
|
|
540
|
+
if (s.target.case === "agent") {
|
|
541
|
+
s.target.value.message = v.trim();
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
isSaving={isUpdating}
|
|
546
|
+
error={
|
|
547
|
+
saveError?.field === "message" ? saveError.message : undefined
|
|
548
|
+
}
|
|
549
|
+
validate={validateMessage}
|
|
550
|
+
/>
|
|
551
|
+
) : (
|
|
552
|
+
<p className="whitespace-pre-wrap break-words text-sm text-foreground">
|
|
553
|
+
{target?.message || "—"}
|
|
554
|
+
</p>
|
|
555
|
+
)}
|
|
556
|
+
</DetailRow>
|
|
557
|
+
<DetailRow label="Environments">
|
|
558
|
+
{editable && target ? (
|
|
559
|
+
<EnvironmentsInlineEditor
|
|
560
|
+
org={scheduleOrg}
|
|
561
|
+
refs={target.environmentRefs ?? []}
|
|
562
|
+
onSave={(refs) =>
|
|
563
|
+
saveSpecField("environments", (s) => {
|
|
564
|
+
if (s.target.case === "agent") {
|
|
565
|
+
s.target.value.environmentRefs = refs.map((r) =>
|
|
566
|
+
create(ApiResourceReferenceSchema, {
|
|
567
|
+
org: r.org,
|
|
568
|
+
slug: r.slug,
|
|
569
|
+
kind: ApiResourceKind.environment,
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
isSaving={isUpdating}
|
|
576
|
+
error={
|
|
577
|
+
saveError?.field === "environments"
|
|
578
|
+
? saveError.message
|
|
579
|
+
: undefined
|
|
580
|
+
}
|
|
581
|
+
/>
|
|
582
|
+
) : (
|
|
583
|
+
<EnvironmentRefList
|
|
584
|
+
refs={target?.environmentRefs ?? []}
|
|
585
|
+
/>
|
|
586
|
+
)}
|
|
587
|
+
</DetailRow>
|
|
588
|
+
{/* Workspace is authored in the creation form (or YAML); an
|
|
589
|
+
inline workspace editor is deliberately deferred — the
|
|
590
|
+
read view keeps every stored entry visible. */}
|
|
591
|
+
<DetailRow label="Workspace">
|
|
592
|
+
<WorkspaceSummary
|
|
593
|
+
entries={target?.workspaceEntries ?? []}
|
|
594
|
+
/>
|
|
595
|
+
</DetailRow>
|
|
596
|
+
<DetailRow label="Engine & model">
|
|
597
|
+
{editable && target ? (
|
|
598
|
+
<EngineModelInlineEditor
|
|
599
|
+
invocation={target}
|
|
600
|
+
onSave={(harness, modelName, serviceTier) =>
|
|
601
|
+
saveSpecField("engine-model", (s) => {
|
|
602
|
+
if (s.target.case === "agent") {
|
|
603
|
+
applyEngineModel(s.target.value, harness, modelName, serviceTier);
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
isSaving={isUpdating}
|
|
608
|
+
error={
|
|
609
|
+
saveError?.field === "engine-model"
|
|
610
|
+
? saveError.message
|
|
611
|
+
: undefined
|
|
612
|
+
}
|
|
613
|
+
/>
|
|
614
|
+
) : (
|
|
615
|
+
<EngineModelSummary invocation={target} />
|
|
616
|
+
)}
|
|
617
|
+
</DetailRow>
|
|
618
|
+
<DetailRow label="Budget per run">
|
|
619
|
+
{editable && target ? (
|
|
620
|
+
<BudgetInlineEditor
|
|
621
|
+
config={target.runConfig}
|
|
622
|
+
onSave={(maxCostUsd) =>
|
|
623
|
+
saveSpecField("budget", (s) => {
|
|
624
|
+
if (s.target.case === "agent") {
|
|
625
|
+
applyBudget(s.target.value, maxCostUsd);
|
|
626
|
+
}
|
|
627
|
+
})
|
|
628
|
+
}
|
|
629
|
+
isSaving={isUpdating}
|
|
630
|
+
error={
|
|
631
|
+
saveError?.field === "budget" ? saveError.message : undefined
|
|
632
|
+
}
|
|
633
|
+
/>
|
|
634
|
+
) : (
|
|
635
|
+
<BudgetSummary config={target?.runConfig} />
|
|
636
|
+
)}
|
|
637
|
+
</DetailRow>
|
|
638
|
+
</dl>
|
|
639
|
+
</Section>
|
|
640
|
+
|
|
641
|
+
<Section title="Status">
|
|
642
|
+
<dl className="divide-y divide-border">
|
|
643
|
+
<DetailRow label="Next fire">
|
|
644
|
+
<span className="text-sm text-foreground">
|
|
645
|
+
{stateInfo.state === "active" && status?.nextFireAt
|
|
646
|
+
? formatNextFire(timestampDate(status.nextFireAt), renderNow)
|
|
647
|
+
: "—"}
|
|
648
|
+
</span>
|
|
649
|
+
</DetailRow>
|
|
650
|
+
<DetailRow label="Last fired">
|
|
651
|
+
<span className="text-sm text-foreground">
|
|
652
|
+
{status?.lastFireAt
|
|
653
|
+
? formatRelativeTime(
|
|
654
|
+
timestampDate(status.lastFireAt),
|
|
655
|
+
renderNow,
|
|
656
|
+
)
|
|
657
|
+
: "Never"}
|
|
658
|
+
</span>
|
|
659
|
+
</DetailRow>
|
|
660
|
+
<DetailRow label="Last execution">
|
|
661
|
+
{status?.lastExecutionId ? (
|
|
662
|
+
<ReferenceLink
|
|
663
|
+
label={status.lastExecutionId}
|
|
664
|
+
mono
|
|
665
|
+
onNavigate={
|
|
666
|
+
onNavigateToExecution
|
|
667
|
+
? () => onNavigateToExecution(status.lastExecutionId)
|
|
668
|
+
: undefined
|
|
669
|
+
}
|
|
670
|
+
/>
|
|
671
|
+
) : (
|
|
672
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
673
|
+
)}
|
|
674
|
+
</DetailRow>
|
|
675
|
+
<DetailRow label="Failure streak">
|
|
676
|
+
<FailureStreak count={status?.consecutiveFailures ?? 0} />
|
|
677
|
+
</DetailRow>
|
|
678
|
+
</dl>
|
|
679
|
+
</Section>
|
|
680
|
+
|
|
681
|
+
<Section
|
|
682
|
+
title="Recent runs"
|
|
683
|
+
headerActions={
|
|
684
|
+
totalRunCount > recentRuns.length ? (
|
|
685
|
+
<button
|
|
686
|
+
type="button"
|
|
687
|
+
onClick={() => effectiveOnTabChange(RUNS_TAB_ID)}
|
|
688
|
+
className={cn(
|
|
689
|
+
"text-xs font-medium text-primary underline-offset-2 hover:underline",
|
|
690
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
|
|
691
|
+
)}
|
|
692
|
+
>
|
|
693
|
+
View all {totalRunCount} runs
|
|
694
|
+
</button>
|
|
695
|
+
) : undefined
|
|
696
|
+
}
|
|
697
|
+
>
|
|
698
|
+
<ScheduleRunsCompactList
|
|
699
|
+
runs={recentRuns}
|
|
700
|
+
isLoading={recentRunsLoading}
|
|
701
|
+
now={renderNow}
|
|
702
|
+
onNavigateToExecution={onNavigateToExecution}
|
|
703
|
+
/>
|
|
704
|
+
</Section>
|
|
705
|
+
</div>
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
let tabContent: React.ReactNode;
|
|
709
|
+
if (activeAdditionalTab) {
|
|
710
|
+
tabContent = activeAdditionalTab.content;
|
|
711
|
+
} else if (effectiveActiveTab === RUNS_TAB_ID) {
|
|
712
|
+
tabContent = (
|
|
713
|
+
<ScheduleRunsTable
|
|
714
|
+
key={runsVersion}
|
|
715
|
+
scheduleId={scheduleId}
|
|
716
|
+
now={now}
|
|
717
|
+
onNavigateToExecution={onNavigateToExecution}
|
|
718
|
+
/>
|
|
719
|
+
);
|
|
720
|
+
} else {
|
|
721
|
+
tabContent = overviewContent;
|
|
722
|
+
}
|
|
723
|
+
|
|
274
724
|
return (
|
|
275
725
|
<>
|
|
276
726
|
<ResourceDetailShell
|
|
@@ -278,96 +728,13 @@ export function ScheduleDetailView({
|
|
|
278
728
|
headerBanner={headerBanner}
|
|
279
729
|
primaryAction={primaryAction}
|
|
280
730
|
actions={actions}
|
|
731
|
+
tabs={effectiveTabs}
|
|
732
|
+
activeTab={effectiveTabs ? effectiveActiveTab : undefined}
|
|
733
|
+
onTabChange={effectiveTabs ? effectiveOnTabChange : undefined}
|
|
734
|
+
tabsAriaLabel="Schedule detail sections"
|
|
281
735
|
className={className}
|
|
282
736
|
>
|
|
283
|
-
|
|
284
|
-
<Section title="Definition">
|
|
285
|
-
<dl className="divide-y divide-border">
|
|
286
|
-
<DetailRow label="Cron">
|
|
287
|
-
<code className="font-mono text-sm text-foreground">
|
|
288
|
-
{spec?.cron || "—"}
|
|
289
|
-
</code>
|
|
290
|
-
</DetailRow>
|
|
291
|
-
<DetailRow label="Time zone">
|
|
292
|
-
<span className="text-sm text-foreground">
|
|
293
|
-
{spec?.timeZone || "—"}
|
|
294
|
-
</span>
|
|
295
|
-
</DetailRow>
|
|
296
|
-
<DetailRow label="Target agent">
|
|
297
|
-
{target?.agentRef ? (
|
|
298
|
-
<ReferenceLink
|
|
299
|
-
label={`${target.agentRef.org}/${target.agentRef.slug}`}
|
|
300
|
-
onNavigate={
|
|
301
|
-
onNavigateToAgent
|
|
302
|
-
? () =>
|
|
303
|
-
onNavigateToAgent(
|
|
304
|
-
target.agentRef!.org,
|
|
305
|
-
target.agentRef!.slug,
|
|
306
|
-
)
|
|
307
|
-
: undefined
|
|
308
|
-
}
|
|
309
|
-
/>
|
|
310
|
-
) : (
|
|
311
|
-
<span className="text-sm text-muted-foreground">—</span>
|
|
312
|
-
)}
|
|
313
|
-
</DetailRow>
|
|
314
|
-
<DetailRow label="Message">
|
|
315
|
-
<p className="whitespace-pre-wrap break-words text-sm text-foreground">
|
|
316
|
-
{target?.message || "—"}
|
|
317
|
-
</p>
|
|
318
|
-
</DetailRow>
|
|
319
|
-
</dl>
|
|
320
|
-
</Section>
|
|
321
|
-
|
|
322
|
-
<Section title="Status">
|
|
323
|
-
<dl className="divide-y divide-border">
|
|
324
|
-
<DetailRow label="Next fire">
|
|
325
|
-
<span className="text-sm text-foreground">
|
|
326
|
-
{stateInfo.state === "active" && status?.nextFireAt
|
|
327
|
-
? formatNextFire(timestampDate(status.nextFireAt), renderNow)
|
|
328
|
-
: "—"}
|
|
329
|
-
</span>
|
|
330
|
-
</DetailRow>
|
|
331
|
-
<DetailRow label="Last fired">
|
|
332
|
-
<span className="text-sm text-foreground">
|
|
333
|
-
{status?.lastFireAt
|
|
334
|
-
? formatRelativeTime(
|
|
335
|
-
timestampDate(status.lastFireAt),
|
|
336
|
-
renderNow,
|
|
337
|
-
)
|
|
338
|
-
: "Never"}
|
|
339
|
-
</span>
|
|
340
|
-
</DetailRow>
|
|
341
|
-
<DetailRow label="Last execution">
|
|
342
|
-
{status?.lastExecutionId ? (
|
|
343
|
-
<ReferenceLink
|
|
344
|
-
label={status.lastExecutionId}
|
|
345
|
-
mono
|
|
346
|
-
onNavigate={
|
|
347
|
-
onNavigateToExecution
|
|
348
|
-
? () => onNavigateToExecution(status.lastExecutionId)
|
|
349
|
-
: undefined
|
|
350
|
-
}
|
|
351
|
-
/>
|
|
352
|
-
) : (
|
|
353
|
-
<span className="text-sm text-muted-foreground">—</span>
|
|
354
|
-
)}
|
|
355
|
-
</DetailRow>
|
|
356
|
-
<DetailRow label="Consecutive failures">
|
|
357
|
-
<span
|
|
358
|
-
className={cn(
|
|
359
|
-
"text-sm",
|
|
360
|
-
(status?.consecutiveFailures ?? 0) > 0
|
|
361
|
-
? "font-medium text-warning"
|
|
362
|
-
: "text-foreground",
|
|
363
|
-
)}
|
|
364
|
-
>
|
|
365
|
-
{status?.consecutiveFailures ?? 0}
|
|
366
|
-
</span>
|
|
367
|
-
</DetailRow>
|
|
368
|
-
</dl>
|
|
369
|
-
</Section>
|
|
370
|
-
</div>
|
|
737
|
+
{tabContent}
|
|
371
738
|
</ResourceDetailShell>
|
|
372
739
|
|
|
373
740
|
<ConfirmDialog
|
|
@@ -437,6 +804,526 @@ function StateBanner({
|
|
|
437
804
|
);
|
|
438
805
|
}
|
|
439
806
|
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
// Cadence summary — plain English first, raw cron as the precise record
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Render a schedule's cadence for reading: the plain-English sentence
|
|
813
|
+
* first ("Every day at 09:00 (Asia/Kolkata)"), the raw cron beneath it
|
|
814
|
+
* in muted mono as the precise record.
|
|
815
|
+
*
|
|
816
|
+
* `cronToCadence` RECOGNIZES rather than parses (the platform owns no
|
|
817
|
+
* cron parser in either edition — see cadence.ts): expressions outside
|
|
818
|
+
* the builder's shapes come back as `custom`, for which the raw cron IS
|
|
819
|
+
* the primary display, with the time zone alongside so nothing the
|
|
820
|
+
* spec stores is hidden.
|
|
821
|
+
*/
|
|
822
|
+
function CadenceSummary({
|
|
823
|
+
cron,
|
|
824
|
+
timeZone,
|
|
825
|
+
}: {
|
|
826
|
+
readonly cron: string;
|
|
827
|
+
readonly timeZone: string;
|
|
828
|
+
}) {
|
|
829
|
+
if (!cron) return <span className="text-sm text-muted-foreground">—</span>;
|
|
830
|
+
|
|
831
|
+
const preset = cronToCadence(cron);
|
|
832
|
+
if (preset.kind === "custom") {
|
|
833
|
+
return (
|
|
834
|
+
<div className="flex flex-col gap-0.5">
|
|
835
|
+
<code className="font-mono text-sm text-foreground">{cron}</code>
|
|
836
|
+
{timeZone && (
|
|
837
|
+
<span className="text-xs text-muted-foreground">{timeZone}</span>
|
|
838
|
+
)}
|
|
839
|
+
</div>
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return (
|
|
843
|
+
<div className="flex flex-col gap-0.5">
|
|
844
|
+
<span className="text-sm text-foreground">
|
|
845
|
+
{describeCadence(preset, timeZone || undefined)}
|
|
846
|
+
</span>
|
|
847
|
+
<code className="font-mono text-xs text-muted-foreground">{cron}</code>
|
|
848
|
+
</div>
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
// Inline editors — per-field click-to-edit over the lossless write path
|
|
854
|
+
// ---------------------------------------------------------------------------
|
|
855
|
+
//
|
|
856
|
+
// Each editor follows the InlineEdit* family's contract: read mode is a
|
|
857
|
+
// click target with a hover pencil; edit mode holds a local draft with
|
|
858
|
+
// explicit Save/Cancel; a failed save keeps the editor open with the
|
|
859
|
+
// server's message rendered verbatim beneath it (DD-006). The editors
|
|
860
|
+
// stay in this file (the AgentDetailView single-organism precedent) and
|
|
861
|
+
// reuse the creation form's field components — CadenceField,
|
|
862
|
+
// TimeZoneField, EnvironmentPicker — so creating and editing a schedule
|
|
863
|
+
// are the same experience.
|
|
864
|
+
|
|
865
|
+
/** Cadence + time zone edit in one panel — they form one sentence. */
|
|
866
|
+
function CadenceInlineEditor({
|
|
867
|
+
cron,
|
|
868
|
+
timeZone,
|
|
869
|
+
onSave,
|
|
870
|
+
isSaving,
|
|
871
|
+
error,
|
|
872
|
+
}: {
|
|
873
|
+
readonly cron: string;
|
|
874
|
+
readonly timeZone: string;
|
|
875
|
+
readonly onSave: (cron: string, timeZone: string) => Promise<boolean>;
|
|
876
|
+
readonly isSaving: boolean;
|
|
877
|
+
readonly error?: string;
|
|
878
|
+
}) {
|
|
879
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
880
|
+
const [draftCadence, setDraftCadence] = useState<CadencePreset>(() =>
|
|
881
|
+
cronToCadence(cron),
|
|
882
|
+
);
|
|
883
|
+
const [draftZone, setDraftZone] = useState(timeZone);
|
|
884
|
+
|
|
885
|
+
const startEdit = () => {
|
|
886
|
+
// cronToCadence round-trips the stored cron into the preset picker;
|
|
887
|
+
// unrecognized shapes land on the Custom escape hatch with the raw
|
|
888
|
+
// string intact.
|
|
889
|
+
setDraftCadence(cronToCadence(cron));
|
|
890
|
+
setDraftZone(timeZone || browserTimeZone());
|
|
891
|
+
setIsEditing(true);
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
if (!isEditing) {
|
|
895
|
+
return (
|
|
896
|
+
<InlineReadButton onEdit={startEdit} ariaLabel="Edit cadence">
|
|
897
|
+
<CadenceSummary cron={cron} timeZone={timeZone} />
|
|
898
|
+
</InlineReadButton>
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const draftCron = cadenceToCron(draftCadence).trim();
|
|
903
|
+
const canSave = draftCron !== "" && validateCron(draftCron) === null;
|
|
904
|
+
|
|
905
|
+
return (
|
|
906
|
+
<div className="flex flex-col gap-2">
|
|
907
|
+
<CadenceField
|
|
908
|
+
value={draftCadence}
|
|
909
|
+
onChange={setDraftCadence}
|
|
910
|
+
timeZone={draftZone}
|
|
911
|
+
disabled={isSaving}
|
|
912
|
+
/>
|
|
913
|
+
<div className="space-y-1">
|
|
914
|
+
<span className={editorLabelClasses}>Time zone</span>
|
|
915
|
+
<TimeZoneField
|
|
916
|
+
value={draftZone}
|
|
917
|
+
onChange={setDraftZone}
|
|
918
|
+
disabled={isSaving}
|
|
919
|
+
/>
|
|
920
|
+
</div>
|
|
921
|
+
<InlineEditActions
|
|
922
|
+
onCancel={() => setIsEditing(false)}
|
|
923
|
+
onSave={async () => {
|
|
924
|
+
const ok = await onSave(draftCron, draftZone);
|
|
925
|
+
if (ok) setIsEditing(false);
|
|
926
|
+
}}
|
|
927
|
+
isSaving={isSaving}
|
|
928
|
+
canSave={canSave}
|
|
929
|
+
error={error}
|
|
930
|
+
/>
|
|
931
|
+
</div>
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Environment bindings edit — org-shared credentials only (DD-017 D-2). */
|
|
936
|
+
function EnvironmentsInlineEditor({
|
|
937
|
+
org,
|
|
938
|
+
refs,
|
|
939
|
+
onSave,
|
|
940
|
+
isSaving,
|
|
941
|
+
error,
|
|
942
|
+
}: {
|
|
943
|
+
readonly org: string;
|
|
944
|
+
readonly refs: readonly { org: string; slug: string }[];
|
|
945
|
+
readonly onSave: (refs: readonly ResourceRef[]) => Promise<boolean>;
|
|
946
|
+
readonly isSaving: boolean;
|
|
947
|
+
readonly error?: string;
|
|
948
|
+
}) {
|
|
949
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
950
|
+
const [draft, setDraft] = useState<ResourceRef[]>([]);
|
|
951
|
+
|
|
952
|
+
const startEdit = () => {
|
|
953
|
+
setDraft(refs.map((r) => ({ org: r.org, slug: r.slug })));
|
|
954
|
+
setIsEditing(true);
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
if (!isEditing) {
|
|
958
|
+
return (
|
|
959
|
+
<InlineReadButton onEdit={startEdit} ariaLabel="Edit environments">
|
|
960
|
+
<EnvironmentRefList refs={refs} />
|
|
961
|
+
</InlineReadButton>
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return (
|
|
966
|
+
<div className="flex flex-col gap-2">
|
|
967
|
+
<EnvironmentPicker
|
|
968
|
+
org={org}
|
|
969
|
+
value={draft}
|
|
970
|
+
onChange={setDraft}
|
|
971
|
+
disabled={isSaving}
|
|
972
|
+
// Only org-shared environments resolve for a schedule fire (the
|
|
973
|
+
// same credential surface a channel binding uses), so offering
|
|
974
|
+
// anything else could only produce refused runs.
|
|
975
|
+
filterEnvironment={isOrgSharedEnvironment}
|
|
976
|
+
/>
|
|
977
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
978
|
+
Bind org-shared credentials so the agent’s tools work on an
|
|
979
|
+
unattended fire. Without this, an agent whose tools need credentials
|
|
980
|
+
will be refused every run.
|
|
981
|
+
</p>
|
|
982
|
+
<InlineEditActions
|
|
983
|
+
onCancel={() => setIsEditing(false)}
|
|
984
|
+
onSave={async () => {
|
|
985
|
+
const ok = await onSave(draft);
|
|
986
|
+
if (ok) setIsEditing(false);
|
|
987
|
+
}}
|
|
988
|
+
isSaving={isSaving}
|
|
989
|
+
canSave
|
|
990
|
+
error={error}
|
|
991
|
+
/>
|
|
992
|
+
</div>
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function isOrgSharedEnvironment(env: Environment): boolean {
|
|
997
|
+
return env.metadata?.visibility === ApiResourceVisibility.visibility_org;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Engine & model edit — the composer's own picker, with the creation
|
|
1002
|
+
* form's atomic semantics (DD-018 D-5): picking a model pins BOTH the
|
|
1003
|
+
* harness and the model (the registry scopes models per harness);
|
|
1004
|
+
* clearing the model unpins both, and the platform defaults apply.
|
|
1005
|
+
*/
|
|
1006
|
+
function EngineModelInlineEditor({
|
|
1007
|
+
invocation,
|
|
1008
|
+
onSave,
|
|
1009
|
+
isSaving,
|
|
1010
|
+
error,
|
|
1011
|
+
}: {
|
|
1012
|
+
readonly invocation: AgentInvocation;
|
|
1013
|
+
readonly onSave: (
|
|
1014
|
+
harness: Harness,
|
|
1015
|
+
modelName: string,
|
|
1016
|
+
serviceTier: ServiceTierOption,
|
|
1017
|
+
) => Promise<boolean>;
|
|
1018
|
+
readonly isSaving: boolean;
|
|
1019
|
+
readonly error?: string;
|
|
1020
|
+
}) {
|
|
1021
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
1022
|
+
const [modelName, setModelName] = useState("");
|
|
1023
|
+
const [harness, setHarness] = useState<HarnessOption>("cursor");
|
|
1024
|
+
const [serviceTier, setServiceTier] = useState<ServiceTierOption>("standard");
|
|
1025
|
+
|
|
1026
|
+
const startEdit = () => {
|
|
1027
|
+
setModelName(invocation.runConfig?.modelName ?? "");
|
|
1028
|
+
setHarness(
|
|
1029
|
+
invocation.harness !== Harness.UNSPECIFIED
|
|
1030
|
+
? fromProtoHarness(invocation.harness)
|
|
1031
|
+
: "cursor",
|
|
1032
|
+
);
|
|
1033
|
+
setServiceTier(
|
|
1034
|
+
fromProtoServiceTier(invocation.runConfig?.serviceTier) ?? "standard",
|
|
1035
|
+
);
|
|
1036
|
+
setIsEditing(true);
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
if (!isEditing) {
|
|
1040
|
+
return (
|
|
1041
|
+
<InlineReadButton onEdit={startEdit} ariaLabel="Edit engine and model">
|
|
1042
|
+
<EngineModelSummary invocation={invocation} />
|
|
1043
|
+
</InlineReadButton>
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return (
|
|
1048
|
+
<div className="flex flex-col gap-2">
|
|
1049
|
+
<div className="flex items-center gap-2">
|
|
1050
|
+
<ModelSelector
|
|
1051
|
+
value={modelName}
|
|
1052
|
+
onValueChange={setModelName}
|
|
1053
|
+
initialHarness={harness}
|
|
1054
|
+
onHarnessChange={setHarness}
|
|
1055
|
+
serviceTier={serviceTier}
|
|
1056
|
+
onServiceTierChange={setServiceTier}
|
|
1057
|
+
placeholderLabel="Platform default"
|
|
1058
|
+
disabled={isSaving}
|
|
1059
|
+
/>
|
|
1060
|
+
{modelName !== "" && (
|
|
1061
|
+
<button
|
|
1062
|
+
type="button"
|
|
1063
|
+
onClick={() => {
|
|
1064
|
+
setModelName("");
|
|
1065
|
+
setServiceTier("standard");
|
|
1066
|
+
}}
|
|
1067
|
+
disabled={isSaving}
|
|
1068
|
+
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"
|
|
1069
|
+
>
|
|
1070
|
+
Reset to platform default
|
|
1071
|
+
</button>
|
|
1072
|
+
)}
|
|
1073
|
+
</div>
|
|
1074
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1075
|
+
Runs use the platform’s default engine and model unless you pick
|
|
1076
|
+
one here. Picking a model pins the engine it belongs to.
|
|
1077
|
+
</p>
|
|
1078
|
+
<InlineEditActions
|
|
1079
|
+
onCancel={() => setIsEditing(false)}
|
|
1080
|
+
onSave={async () => {
|
|
1081
|
+
const ok = await onSave(
|
|
1082
|
+
modelName !== "" ? toProtoHarness(harness) : Harness.UNSPECIFIED,
|
|
1083
|
+
modelName,
|
|
1084
|
+
serviceTier,
|
|
1085
|
+
);
|
|
1086
|
+
if (ok) setIsEditing(false);
|
|
1087
|
+
}}
|
|
1088
|
+
isSaving={isSaving}
|
|
1089
|
+
canSave
|
|
1090
|
+
error={error}
|
|
1091
|
+
/>
|
|
1092
|
+
</div>
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** Budget edit — blank inherits the platform default (DD-018 D-2). */
|
|
1097
|
+
function BudgetInlineEditor({
|
|
1098
|
+
config,
|
|
1099
|
+
onSave,
|
|
1100
|
+
isSaving,
|
|
1101
|
+
error,
|
|
1102
|
+
}: {
|
|
1103
|
+
readonly config: RunConfig | undefined;
|
|
1104
|
+
readonly onSave: (maxCostUsd: number | undefined) => Promise<boolean>;
|
|
1105
|
+
readonly isSaving: boolean;
|
|
1106
|
+
readonly error?: string;
|
|
1107
|
+
}) {
|
|
1108
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
1109
|
+
const [budgetUsd, setBudgetUsd] = useState("");
|
|
1110
|
+
|
|
1111
|
+
const startEdit = () => {
|
|
1112
|
+
setBudgetUsd(config && config.maxCostUsd > 0 ? String(config.maxCostUsd) : "");
|
|
1113
|
+
setIsEditing(true);
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
if (!isEditing) {
|
|
1117
|
+
return (
|
|
1118
|
+
<InlineReadButton onEdit={startEdit} ariaLabel="Edit budget">
|
|
1119
|
+
<BudgetSummary config={config} />
|
|
1120
|
+
</InlineReadButton>
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const cost = Number.parseFloat(budgetUsd);
|
|
1125
|
+
const parsedBudget = Number.isFinite(cost) && cost > 0 ? cost : undefined;
|
|
1126
|
+
|
|
1127
|
+
return (
|
|
1128
|
+
<div className="flex flex-col gap-2">
|
|
1129
|
+
<input
|
|
1130
|
+
type="number"
|
|
1131
|
+
min="0"
|
|
1132
|
+
step="any"
|
|
1133
|
+
aria-label="Budget per run (USD)"
|
|
1134
|
+
value={budgetUsd}
|
|
1135
|
+
onChange={(e) => setBudgetUsd(e.target.value)}
|
|
1136
|
+
placeholder="platform default"
|
|
1137
|
+
disabled={isSaving}
|
|
1138
|
+
className={cn(editorInputClasses, "sm:max-w-48")}
|
|
1139
|
+
/>
|
|
1140
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1141
|
+
Each run stops when it reaches this spend. You can lower the
|
|
1142
|
+
platform’s per-run cap, never raise it past the
|
|
1143
|
+
platform’s ceiling. Blank inherits the platform default.
|
|
1144
|
+
</p>
|
|
1145
|
+
<InlineEditActions
|
|
1146
|
+
onCancel={() => setIsEditing(false)}
|
|
1147
|
+
onSave={async () => {
|
|
1148
|
+
const ok = await onSave(parsedBudget);
|
|
1149
|
+
if (ok) setIsEditing(false);
|
|
1150
|
+
}}
|
|
1151
|
+
isSaving={isSaving}
|
|
1152
|
+
canSave
|
|
1153
|
+
error={error}
|
|
1154
|
+
/>
|
|
1155
|
+
</div>
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Write the engine+model choice onto the invocation, preserving the
|
|
1161
|
+
* run-config fields the editor does not own (budget; the API-only tool
|
|
1162
|
+
* rounds), and dropping an all-empty run_config — the proto's "empty =
|
|
1163
|
+
* inherit" contract (DD-017 D-3 as carried into DD-018 D-2).
|
|
1164
|
+
*/
|
|
1165
|
+
function applyEngineModel(
|
|
1166
|
+
invocation: AgentInvocation,
|
|
1167
|
+
harness: Harness,
|
|
1168
|
+
modelName: string,
|
|
1169
|
+
serviceTier: ServiceTierOption,
|
|
1170
|
+
): void {
|
|
1171
|
+
invocation.harness = harness;
|
|
1172
|
+
invocation.runConfig = normalizeRunConfig(
|
|
1173
|
+
modelName,
|
|
1174
|
+
invocation.runConfig?.maxCostUsd ?? 0,
|
|
1175
|
+
invocation.runConfig?.maxToolRounds ?? 0,
|
|
1176
|
+
// The tier rides the model choice: no model, no tier (#357).
|
|
1177
|
+
modelName.trim() !== "" ? serviceTier : "standard",
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** Budget twin of {@link applyEngineModel} — writes only the cost cap. */
|
|
1182
|
+
function applyBudget(
|
|
1183
|
+
invocation: AgentInvocation,
|
|
1184
|
+
maxCostUsd: number | undefined,
|
|
1185
|
+
): void {
|
|
1186
|
+
invocation.runConfig = normalizeRunConfig(
|
|
1187
|
+
invocation.runConfig?.modelName ?? "",
|
|
1188
|
+
maxCostUsd ?? 0,
|
|
1189
|
+
invocation.runConfig?.maxToolRounds ?? 0,
|
|
1190
|
+
fromProtoServiceTier(invocation.runConfig?.serviceTier) ?? "standard",
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function normalizeRunConfig(
|
|
1195
|
+
modelName: string,
|
|
1196
|
+
maxCostUsd: number,
|
|
1197
|
+
maxToolRounds: number,
|
|
1198
|
+
serviceTier: ServiceTierOption,
|
|
1199
|
+
): RunConfig | undefined {
|
|
1200
|
+
const fields: {
|
|
1201
|
+
modelName?: string;
|
|
1202
|
+
maxCostUsd?: number;
|
|
1203
|
+
maxToolRounds?: number;
|
|
1204
|
+
serviceTier?: ServiceTier;
|
|
1205
|
+
} = {};
|
|
1206
|
+
if (modelName.trim() !== "") fields.modelName = modelName.trim();
|
|
1207
|
+
if (maxCostUsd > 0) fields.maxCostUsd = maxCostUsd;
|
|
1208
|
+
if (maxToolRounds > 0) fields.maxToolRounds = maxToolRounds;
|
|
1209
|
+
// Only an active fast choice is carried — an untouched toggle stays
|
|
1210
|
+
// absent, preserving the unspecified-vs-explicit ledger distinction (#357).
|
|
1211
|
+
if (serviceTier === "fast") fields.serviceTier = toProtoServiceTier(serviceTier);
|
|
1212
|
+
|
|
1213
|
+
return Object.keys(fields).length > 0
|
|
1214
|
+
? create(RunConfigSchema, fields)
|
|
1215
|
+
: undefined;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/** Mirrors the server's constraint so the editor rejects bad input instantly. */
|
|
1219
|
+
function validateMessage(value: string): string | null {
|
|
1220
|
+
if (!value.trim()) {
|
|
1221
|
+
return "Message is required — the agent receives it on every fire.";
|
|
1222
|
+
}
|
|
1223
|
+
if (value.length > MESSAGE_MAX_LEN) {
|
|
1224
|
+
return `Message must be at most ${MESSAGE_MAX_LEN} characters.`;
|
|
1225
|
+
}
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// ---------------------------------------------------------------------------
|
|
1230
|
+
// Inline-edit chrome shared by the bespoke editors above
|
|
1231
|
+
// ---------------------------------------------------------------------------
|
|
1232
|
+
|
|
1233
|
+
const editorLabelClasses = "block text-xs font-medium text-foreground";
|
|
1234
|
+
|
|
1235
|
+
const editorInputClasses = cn(
|
|
1236
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
1237
|
+
"placeholder:text-muted-foreground",
|
|
1238
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
1239
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
/** Read-mode click target with the family's hover pencil. */
|
|
1243
|
+
function InlineReadButton({
|
|
1244
|
+
onEdit,
|
|
1245
|
+
ariaLabel,
|
|
1246
|
+
children,
|
|
1247
|
+
}: {
|
|
1248
|
+
readonly onEdit: () => void;
|
|
1249
|
+
readonly ariaLabel: string;
|
|
1250
|
+
readonly children: React.ReactNode;
|
|
1251
|
+
}) {
|
|
1252
|
+
return (
|
|
1253
|
+
<div className="group/inline-edit">
|
|
1254
|
+
<button
|
|
1255
|
+
type="button"
|
|
1256
|
+
onClick={onEdit}
|
|
1257
|
+
aria-label={ariaLabel}
|
|
1258
|
+
className={cn(
|
|
1259
|
+
"-mx-2 w-full rounded-md px-2 py-1.5 text-left transition-colors",
|
|
1260
|
+
"hover:bg-accent-hover cursor-pointer",
|
|
1261
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
1262
|
+
)}
|
|
1263
|
+
>
|
|
1264
|
+
<div className="flex items-start justify-between gap-2">
|
|
1265
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
1266
|
+
<PencilIcon className="mt-0.5 size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/inline-edit:opacity-100" />
|
|
1267
|
+
</div>
|
|
1268
|
+
</button>
|
|
1269
|
+
</div>
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** Save/Cancel footer with the field-attributed error line. */
|
|
1274
|
+
function InlineEditActions({
|
|
1275
|
+
onCancel,
|
|
1276
|
+
onSave,
|
|
1277
|
+
isSaving,
|
|
1278
|
+
canSave,
|
|
1279
|
+
error,
|
|
1280
|
+
}: {
|
|
1281
|
+
readonly onCancel: () => void;
|
|
1282
|
+
readonly onSave: () => void;
|
|
1283
|
+
readonly isSaving: boolean;
|
|
1284
|
+
readonly canSave: boolean;
|
|
1285
|
+
readonly error?: string;
|
|
1286
|
+
}) {
|
|
1287
|
+
return (
|
|
1288
|
+
<div className="flex flex-col gap-1.5">
|
|
1289
|
+
<div className="flex items-center justify-end gap-1.5">
|
|
1290
|
+
<button
|
|
1291
|
+
type="button"
|
|
1292
|
+
onClick={onCancel}
|
|
1293
|
+
disabled={isSaving}
|
|
1294
|
+
className={cn(
|
|
1295
|
+
"rounded-md px-2.5 py-1 text-xs font-medium",
|
|
1296
|
+
"border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
1297
|
+
"disabled:opacity-50",
|
|
1298
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1299
|
+
)}
|
|
1300
|
+
>
|
|
1301
|
+
Cancel
|
|
1302
|
+
</button>
|
|
1303
|
+
<button
|
|
1304
|
+
type="button"
|
|
1305
|
+
onClick={onSave}
|
|
1306
|
+
disabled={!canSave || isSaving}
|
|
1307
|
+
className={cn(
|
|
1308
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
1309
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
1310
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
1311
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1312
|
+
)}
|
|
1313
|
+
>
|
|
1314
|
+
{isSaving && <SpinnerIcon />}
|
|
1315
|
+
Save
|
|
1316
|
+
</button>
|
|
1317
|
+
</div>
|
|
1318
|
+
{error && (
|
|
1319
|
+
<p className="text-xs text-destructive" role="alert">
|
|
1320
|
+
{error}
|
|
1321
|
+
</p>
|
|
1322
|
+
)}
|
|
1323
|
+
</div>
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
440
1327
|
// ---------------------------------------------------------------------------
|
|
441
1328
|
// Detail rows and references
|
|
442
1329
|
// ---------------------------------------------------------------------------
|
|
@@ -458,6 +1345,152 @@ function DetailRow({
|
|
|
458
1345
|
);
|
|
459
1346
|
}
|
|
460
1347
|
|
|
1348
|
+
/** The bound environment references, or the em-dash when there are none. */
|
|
1349
|
+
function EnvironmentRefList({
|
|
1350
|
+
refs,
|
|
1351
|
+
}: {
|
|
1352
|
+
readonly refs: readonly { org: string; slug: string }[];
|
|
1353
|
+
}) {
|
|
1354
|
+
if (refs.length === 0) {
|
|
1355
|
+
return <span className="text-sm text-muted-foreground">—</span>;
|
|
1356
|
+
}
|
|
1357
|
+
return (
|
|
1358
|
+
<ul className="flex flex-col gap-0.5">
|
|
1359
|
+
{refs.map((ref, i) => (
|
|
1360
|
+
<li
|
|
1361
|
+
key={`${ref.org}/${ref.slug}-${i}`}
|
|
1362
|
+
className="font-mono text-xs text-foreground"
|
|
1363
|
+
>
|
|
1364
|
+
{ref.org ? `${ref.org}/${ref.slug}` : ref.slug}
|
|
1365
|
+
</li>
|
|
1366
|
+
))}
|
|
1367
|
+
</ul>
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// ---------------------------------------------------------------------------
|
|
1372
|
+
// Failure streak — status.consecutive_failures, explained
|
|
1373
|
+
// ---------------------------------------------------------------------------
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* The platform's failure streak: how many SCHEDULED runs in a row ended
|
|
1377
|
+
* badly. The server increments it per failed cron fire, resets it on a
|
|
1378
|
+
* successful run (or Resume), and auto-pauses the schedule when the
|
|
1379
|
+
* streak crosses its threshold. Manual "Run now" fires never count.
|
|
1380
|
+
*
|
|
1381
|
+
* The threshold itself is server configuration not exposed through the
|
|
1382
|
+
* API, so the copy stays qualitative — a hardcoded "of 5" here could
|
|
1383
|
+
* silently drift from what the platform actually enforces.
|
|
1384
|
+
*/
|
|
1385
|
+
function FailureStreak({ count }: { readonly count: number }) {
|
|
1386
|
+
if (count === 0) {
|
|
1387
|
+
return <span className="text-sm text-foreground">0</span>;
|
|
1388
|
+
}
|
|
1389
|
+
return (
|
|
1390
|
+
<div className="flex flex-col gap-0.5">
|
|
1391
|
+
<span className="text-sm font-medium text-warning">
|
|
1392
|
+
{count} consecutive failed {count === 1 ? "run" : "runs"}
|
|
1393
|
+
</span>
|
|
1394
|
+
<p className="text-xs text-muted-foreground">
|
|
1395
|
+
Failed scheduled runs raise this streak; too many in a row and the
|
|
1396
|
+
platform pauses the schedule automatically. One successful run
|
|
1397
|
+
resets it to 0. Manual runs never count.
|
|
1398
|
+
</p>
|
|
1399
|
+
</div>
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// ---------------------------------------------------------------------------
|
|
1404
|
+
// The invocation's run shape — engine+model, budget, workspace summaries
|
|
1405
|
+
// ---------------------------------------------------------------------------
|
|
1406
|
+
|
|
1407
|
+
/** "Cursor · composer-2.5", either half falling back to the platform default. */
|
|
1408
|
+
function EngineModelSummary({
|
|
1409
|
+
invocation,
|
|
1410
|
+
}: {
|
|
1411
|
+
readonly invocation: AgentInvocation | undefined;
|
|
1412
|
+
}) {
|
|
1413
|
+
const harness = invocation?.harness ?? Harness.UNSPECIFIED;
|
|
1414
|
+
const modelName = invocation?.runConfig?.modelName ?? "";
|
|
1415
|
+
|
|
1416
|
+
if (harness === Harness.UNSPECIFIED && modelName === "") {
|
|
1417
|
+
return (
|
|
1418
|
+
<span className="text-sm text-muted-foreground">Platform default</span>
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const parts: string[] = [];
|
|
1423
|
+
if (harness !== Harness.UNSPECIFIED) {
|
|
1424
|
+
parts.push(HARNESS_META[fromProtoHarness(harness)].label);
|
|
1425
|
+
}
|
|
1426
|
+
parts.push(modelName !== "" ? modelName : "platform-default model");
|
|
1427
|
+
if (invocation?.runConfig?.serviceTier === ServiceTier.FAST) {
|
|
1428
|
+
parts.push("Fast tier");
|
|
1429
|
+
}
|
|
1430
|
+
return <span className="text-sm text-foreground">{parts.join(" · ")}</span>;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** The per-run cost cap, plus the API-only tool-round bound when set. */
|
|
1434
|
+
function BudgetSummary({
|
|
1435
|
+
config,
|
|
1436
|
+
}: {
|
|
1437
|
+
readonly config: RunConfig | undefined;
|
|
1438
|
+
}) {
|
|
1439
|
+
const maxCostUsd = config?.maxCostUsd ?? 0;
|
|
1440
|
+
const maxToolRounds = config?.maxToolRounds ?? 0;
|
|
1441
|
+
|
|
1442
|
+
return (
|
|
1443
|
+
<div className="flex flex-col gap-0.5">
|
|
1444
|
+
{maxCostUsd > 0 ? (
|
|
1445
|
+
<span className="text-sm text-foreground">
|
|
1446
|
+
≤ ${maxCostUsd.toFixed(2)} per run
|
|
1447
|
+
</span>
|
|
1448
|
+
) : (
|
|
1449
|
+
<span className="text-sm text-muted-foreground">Platform default</span>
|
|
1450
|
+
)}
|
|
1451
|
+
{/* Reachable through the API only (DD-018 D-5) — rendered when
|
|
1452
|
+
set so nothing the spec stores is hidden. */}
|
|
1453
|
+
{maxToolRounds > 0 && (
|
|
1454
|
+
<span className="text-xs text-muted-foreground">
|
|
1455
|
+
≤ {maxToolRounds} tool rounds
|
|
1456
|
+
</span>
|
|
1457
|
+
)}
|
|
1458
|
+
</div>
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/** The git workspace each fire clones, or the em-dash when none. */
|
|
1463
|
+
function WorkspaceSummary({
|
|
1464
|
+
entries,
|
|
1465
|
+
}: {
|
|
1466
|
+
readonly entries: readonly WorkspaceEntry[];
|
|
1467
|
+
}) {
|
|
1468
|
+
if (entries.length === 0) {
|
|
1469
|
+
return <span className="text-sm text-muted-foreground">—</span>;
|
|
1470
|
+
}
|
|
1471
|
+
return (
|
|
1472
|
+
<ul className="flex flex-col gap-0.5">
|
|
1473
|
+
{entries.map((entry, i) => {
|
|
1474
|
+
const git =
|
|
1475
|
+
entry.source?.source?.case === "gitRepo"
|
|
1476
|
+
? entry.source.source.value
|
|
1477
|
+
: undefined;
|
|
1478
|
+
return (
|
|
1479
|
+
<li key={`${entry.name}-${i}`} className="text-xs text-foreground">
|
|
1480
|
+
<span className="font-medium">{entry.name}</span>
|
|
1481
|
+
{git?.url && (
|
|
1482
|
+
<span className="ml-1.5 font-mono text-muted-foreground">
|
|
1483
|
+
{git.url}
|
|
1484
|
+
{git.branch ? `@${git.branch}` : ""}
|
|
1485
|
+
</span>
|
|
1486
|
+
)}
|
|
1487
|
+
</li>
|
|
1488
|
+
);
|
|
1489
|
+
})}
|
|
1490
|
+
</ul>
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
461
1494
|
function ReferenceLink({
|
|
462
1495
|
label,
|
|
463
1496
|
onNavigate,
|
|
@@ -581,3 +1614,38 @@ function WarningIcon({ className }: { readonly className?: string }) {
|
|
|
581
1614
|
</svg>
|
|
582
1615
|
);
|
|
583
1616
|
}
|
|
1617
|
+
|
|
1618
|
+
function PencilIcon({ className }: { readonly className?: string }) {
|
|
1619
|
+
return (
|
|
1620
|
+
<svg
|
|
1621
|
+
className={className}
|
|
1622
|
+
viewBox="0 0 16 16"
|
|
1623
|
+
fill="none"
|
|
1624
|
+
stroke="currentColor"
|
|
1625
|
+
strokeWidth="1.5"
|
|
1626
|
+
strokeLinecap="round"
|
|
1627
|
+
strokeLinejoin="round"
|
|
1628
|
+
aria-hidden="true"
|
|
1629
|
+
>
|
|
1630
|
+
<path d="M11.5 1.5a2.121 2.121 0 0 1 3 3L5 14l-4 1 1-4Z" />
|
|
1631
|
+
</svg>
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function SpinnerIcon() {
|
|
1636
|
+
return (
|
|
1637
|
+
<svg
|
|
1638
|
+
width="14"
|
|
1639
|
+
height="14"
|
|
1640
|
+
viewBox="0 0 16 16"
|
|
1641
|
+
fill="none"
|
|
1642
|
+
stroke="currentColor"
|
|
1643
|
+
strokeWidth="2"
|
|
1644
|
+
strokeLinecap="round"
|
|
1645
|
+
className="animate-spin"
|
|
1646
|
+
aria-hidden="true"
|
|
1647
|
+
>
|
|
1648
|
+
<path d="M8 2a6 6 0 1 0 6 6" />
|
|
1649
|
+
</svg>
|
|
1650
|
+
);
|
|
1651
|
+
}
|