feat(cron): add Run Continuity toggle + badge (#924) (#932)

Expose backend cron self-continuity (context_from=["self"]) in the Cron
editor. Adds a Run Continuity checkbox to the job editor that sends
context_from=["self"] on create and update (null when off, clearing it),
prefills from the loaded job's context_from, and shows a "Continuity" badge
on recurring jobs in the list. context_from was already on CronJob and
CreateCronJobRequest, so this is editor/list wiring only. Strings added for
en/zh/ko. Includes two ViewModel tests.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-18 12:36:05 +03:00
1 parent 50a0f30d8f
commit 1039d7db1d
6 files changed
+122

No files matched your search

@@ -186,6 +186,14 @@ fun CronJobsScreen(
)
}
}
// Run-continuity badge: the job's own previous
// output is injected into each run.
if (job.context_from?.contains("self") == true) {
StatusBadge(
text = stringResource(R.string.cron_badge_continuity),
status = StatusBadgeType.INFO,
)
}
}
Spacer(modifier = Modifier.height(spacing.xs))
Text(
@@ -267,6 +275,7 @@ fun CronJobsScreen(
state = state.editorState,
onFieldChange = { name, value -> viewModel.updateEditorField(name, value) },
onToggleNoAgent = { viewModel.toggleNoAgent() },
onToggleRunContinuity = { viewModel.toggleRunContinuity() },
onSetMonitorMode = { viewModel.setMonitorMode(it) },
onSelectBlueprint = { viewModel.selectBlueprint(it) },
onBlueprintFieldChange = { name, value -> viewModel.updateBlueprintValue(name, value) },
@@ -331,6 +340,7 @@ fun CronJobEditorDialog(
state: CronJobEditorState,
onFieldChange: (String, String) -> Unit,
onToggleNoAgent: () -> Unit,
onToggleRunContinuity: () -> Unit,
onSetMonitorMode: (String) -> Unit,
onSelectBlueprint: (String?) -> Unit,
onBlueprintFieldChange: (String, String) -> Unit,
@@ -571,6 +581,30 @@ fun CronJobEditorDialog(
)
}
// Run Continuity toggle (recurring jobs: inject own
// previous output into each run via context_from=["self"])
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = state.runContinuity,
onCheckedChange = { onToggleRunContinuity() },
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.cron_edit_field_run_continuity),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringResource(R.string.cron_edit_hint_run_continuity),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
)
}
}
// Monitor mode (disabled for no-agent jobs — the
// backend rejects monitor + no_agent together)
MonitorModeSection(
@@ -58,6 +58,10 @@ data class CronJobEditorState(
val workdir: String = "",
val enabled: Boolean = true,
val no_agent: Boolean = false,
// Run continuity: when true, the job's own previous output is injected
// into each subsequent run via context_from=["self"] (backend cron
// self-continuity). Only meaningful for recurring jobs.
val runContinuity: Boolean = false,
// Monitor mode: one of monitor_script / monitor_url set at a time
// (mutually exclusive on the backend; incompatible with no_agent).
val monitorMode: String = "off", // "off" | "script" | "url"
@@ -309,6 +313,7 @@ class CronJobsViewModel :
workdir = job.workdir.orEmpty(),
enabled = job.enabled ?: true,
no_agent = job.no_agent ?: false,
runContinuity = job.context_from?.contains("self") ?: false,
monitorMode =
when {
!job.monitor_script.isNullOrBlank() -> "script"
@@ -425,6 +430,8 @@ class CronJobsViewModel :
updates["script"] = editor.script.ifBlank { null }
updates["workdir"] = editor.workdir.ifBlank { null }
updates["no_agent"] = editor.no_agent
// Run continuity: send ["self"] when on, null (clears) when off.
updates["context_from"] = if (editor.runContinuity) listOf("self") else null
// Blank clears the monitor source on the backend (empty
// string or null both mean "clear the field").
updates["monitor_script"] = editor.monitor_script.ifBlank { null }
@@ -488,6 +495,7 @@ class CronJobsViewModel :
script = editor.script.ifBlank { null },
workdir = editor.workdir.ifBlank { null },
no_agent = editor.no_agent,
context_from = if (editor.runContinuity) listOf("self") else null,
monitor_script = editor.monitor_script.ifBlank { null },
monitor_url = editor.monitor_url.ifBlank { null },
),
@@ -553,12 +561,25 @@ class CronJobsViewModel :
"base_url" -> copy(base_url = value)
"script" -> copy(script = value)
"workdir" -> copy(workdir = value)
"run_continuity" -> copy(runContinuity = value.toBooleanStrictOrNull() ?: false)
// Monitor fields are mutually exclusive: entering one clears the other.
"monitor_script" -> copy(monitor_script = value, monitor_url = if (value.isNotBlank()) "" else monitor_url)
"monitor_url" -> copy(monitor_url = value, monitor_script = if (value.isNotBlank()) "" else monitor_script)
else -> this
}
fun toggleRunContinuity() {
_uiState.update {
it.copy(
editorState =
it.editorState.copy(
runContinuity = !it.editorState.runContinuity,
toastMessage = null,
),
)
}
}
fun toggleNoAgent() {
_uiState.update {
it.copy(