Skip to content

Commit 5532031

Browse files
committed
feat: add one-time delay feature for task scheduling
- Introduced new strings for one-time delay labels and notes in cockpitWebviewStrings.ts and i18n.ts. - Updated the task editor markup to include a one-time delay input section in cockpitWebviewTaskEditorMarkup.ts. - Enhanced the workspace tabs markup to display metrics and refresh status buttons in cockpitWebviewWorkspaceTabsMarkup.ts. - Implemented logic to handle one-time delay seconds in task creation and updates in extension.ts and cockpitManager.test.ts. - Added tests to validate the one-time delay functionality in cockpitManager.test.ts and cockpitWebview.test.ts. - Updated types to include one-time delay seconds in types.ts. - Added validation for the new refreshStorageStatus message in incomingWebviewMessage.ts.
1 parent e92b19d commit 5532031

22 files changed

Lines changed: 512 additions & 10508 deletions

media/cockpitWebview.js

Lines changed: 191 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
104104
var strings = bootstrapData.strings;
105105
var currentLogLevel = bootstrapData.currentLogLevel;
106106
var currentLogDirectory = bootstrapData.currentLogDirectory;
107+
var storageStatusRefreshNoteTimer = null;
107108

108109
function refreshTaskCountdowns() {
109110
if (!taskList || !taskList.isConnected) {
@@ -388,8 +389,14 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
388389
restoreHistoryBtn,
389390
autoShowStartupNote,
390391
friendlyBuilder,
392+
recurringScheduleGroup,
393+
oneTimeDelayGroup,
391394
cronPreset,
392395
cronExpression,
396+
oneTimeDelayHours,
397+
oneTimeDelayMinutes,
398+
oneTimeDelaySeconds,
399+
oneTimeDelayPreviewText,
393400
agentSelect,
394401
modelSelect,
395402
chatSessionGroup,
@@ -408,6 +415,8 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
408415
syncBundledAgentsBtn,
409416
openCopilotSettingsBtn,
410417
openExtensionSettingsBtn,
418+
refreshStorageStatusBtn,
419+
settingsStatusRefreshNote,
411420
importStorageFromJsonBtn,
412421
exportStorageToJsonBtn,
413422
helpLanguageSelect,
@@ -429,6 +438,7 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
429438
taskFilterBar,
430439
taskLabelFilter,
431440
taskLabelsInput,
441+
runFirstGroup,
432442
jobsFolderList,
433443
jobsCurrentFolderBanner,
434444
jobsList,
@@ -1409,6 +1419,22 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
14091419
}
14101420
}
14111421

1422+
function showStorageStatusRefreshNote() {
1423+
if (!settingsStatusRefreshNote) {
1424+
return;
1425+
}
1426+
settingsStatusRefreshNote.textContent = strings.settingsStatusUpdated || "✓ Updated";
1427+
settingsStatusRefreshNote.style.opacity = "1";
1428+
if (storageStatusRefreshNoteTimer) {
1429+
window.clearTimeout(storageStatusRefreshNoteTimer);
1430+
}
1431+
storageStatusRefreshNoteTimer = window.setTimeout(function () {
1432+
settingsStatusRefreshNote.style.opacity = "0";
1433+
settingsStatusRefreshNote.textContent = "";
1434+
storageStatusRefreshNoteTimer = null;
1435+
}, 2000);
1436+
}
1437+
14121438
function renderLoggingControls() {
14131439
if (settingsLogLevelSelect) {
14141440
settingsLogLevelSelect.value = currentLogLevel || "info";
@@ -1550,9 +1576,30 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
15501576
function syncRecurringChatSessionUi() {
15511577
var oneTimeEl = document.getElementById("one-time");
15521578
var manualSessionEl = document.getElementById("manual-session");
1579+
var runFirstEl = document.getElementById("run-first");
15531580
var isOneTime = !!(oneTimeEl && oneTimeEl.checked);
15541581
var isManualSession = !!(manualSessionEl && manualSessionEl.checked);
15551582

1583+
if (isOneTime && manualSessionEl && manualSessionEl.checked) {
1584+
manualSessionEl.checked = false;
1585+
isManualSession = false;
1586+
}
1587+
1588+
if (isManualSession && oneTimeEl && oneTimeEl.checked) {
1589+
oneTimeEl.checked = false;
1590+
isOneTime = false;
1591+
}
1592+
1593+
if (recurringScheduleGroup) {
1594+
recurringScheduleGroup.style.display = isOneTime ? "none" : "";
1595+
}
1596+
if (oneTimeDelayGroup) {
1597+
oneTimeDelayGroup.style.display = isOneTime ? "block" : "none";
1598+
}
1599+
if (runFirstGroup) {
1600+
runFirstGroup.style.display = isOneTime ? "none" : "block";
1601+
}
1602+
15561603
if (chatSessionGroup) {
15571604
chatSessionGroup.style.display = isOneTime ? "none" : "block";
15581605
}
@@ -1565,13 +1612,94 @@ import { createSchedulerWebviewTransientState } from "./cockpitWebviewTransientS
15651612
chatSessionSelect.value = defaultChatSession;
15661613
}
15671614

1568-
if (isOneTime && manualSessionEl && manualSessionEl.checked) {
1569-
manualSessionEl.checked = false;
1615+
if (isOneTime && runFirstEl && runFirstEl.checked) {
1616+
runFirstEl.checked = false;
15701617
}
15711618

1572-
if (isManualSession && oneTimeEl && oneTimeEl.checked) {
1573-
oneTimeEl.checked = false;
1619+
updateOneTimeDelayPreview();
1620+
}
1621+
1622+
function normalizeOneTimeDelayPart(value, maxValue) {
1623+
var numericValue = typeof value === "number" ? value : Number(value);
1624+
if (!isFinite(numericValue) || numericValue < 0) {
1625+
return 0;
1626+
}
1627+
1628+
var wholeNumber = Math.floor(numericValue);
1629+
if (typeof maxValue === "number") {
1630+
return Math.min(wholeNumber, maxValue);
1631+
}
1632+
return wholeNumber;
1633+
}
1634+
1635+
function getOneTimeDelaySecondsFromInputs() {
1636+
return normalizeOneTimeDelayPart(oneTimeDelayHours ? oneTimeDelayHours.value : 0) * 3600
1637+
+ normalizeOneTimeDelayPart(oneTimeDelayMinutes ? oneTimeDelayMinutes.value : 0, 59) * 60
1638+
+ normalizeOneTimeDelayPart(oneTimeDelaySeconds ? oneTimeDelaySeconds.value : 0, 59);
1639+
}
1640+
1641+
function formatHumanDuration(totalSeconds) {
1642+
var normalizedSeconds = normalizeOneTimeDelayPart(totalSeconds);
1643+
var hours = Math.floor(normalizedSeconds / 3600);
1644+
var minutes = Math.floor((normalizedSeconds % 3600) / 60);
1645+
var seconds = normalizedSeconds % 60;
1646+
if (hours > 0) {
1647+
return minutes > 0
1648+
? hours + " " + (hours === 1 ? "hour" : "hours") + " " + minutes + " " + (minutes === 1 ? "minute" : "minutes")
1649+
: hours + " " + (hours === 1 ? "hour" : "hours");
1650+
}
1651+
if (minutes > 0) {
1652+
return seconds > 0
1653+
? minutes + " " + (minutes === 1 ? "minute" : "minutes") + " " + seconds + " " + (seconds === 1 ? "second" : "seconds")
1654+
: minutes + " " + (minutes === 1 ? "minute" : "minutes");
15741655
}
1656+
return normalizedSeconds + " " + (normalizedSeconds === 1 ? "second" : "seconds");
1657+
}
1658+
1659+
function setOneTimeDelayInputs(totalSeconds) {
1660+
var normalized = normalizeOneTimeDelayPart(totalSeconds);
1661+
if (oneTimeDelayHours) {
1662+
oneTimeDelayHours.value = String(Math.floor(normalized / 3600));
1663+
}
1664+
if (oneTimeDelayMinutes) {
1665+
oneTimeDelayMinutes.value = String(Math.floor((normalized % 3600) / 60));
1666+
}
1667+
if (oneTimeDelaySeconds) {
1668+
oneTimeDelaySeconds.value = String(normalized % 60);
1669+
}
1670+
}
1671+
1672+
function deriveTaskOneTimeDelaySeconds(task) {
1673+
var storedDelay = normalizeOneTimeDelayPart(task && task.oneTimeDelaySeconds);
1674+
if (storedDelay > 0) {
1675+
return storedDelay;
1676+
}
1677+
if (!(task && task.oneTime === true && task.nextRun)) {
1678+
return 0;
1679+
}
1680+
1681+
var nextRunDate = new Date(task.nextRun);
1682+
var remainingSeconds = Math.ceil((nextRunDate.getTime() - Date.now()) / 1000);
1683+
return remainingSeconds > 0 ? remainingSeconds : 0;
1684+
}
1685+
1686+
function updateOneTimeDelayPreview() {
1687+
if (!oneTimeDelayPreviewText) {
1688+
return;
1689+
}
1690+
1691+
var totalSeconds = getOneTimeDelaySecondsFromInputs();
1692+
if (totalSeconds < 1) {
1693+
oneTimeDelayPreviewText.textContent =
1694+
strings.oneTimeDelayPreviewUnset || "Set a delay to schedule this one-time run.";
1695+
return;
1696+
}
1697+
1698+
var nextRunDate = new Date(Date.now() + totalSeconds * 1000);
1699+
oneTimeDelayPreviewText.textContent =
1700+
formatHumanDuration(totalSeconds) +
1701+
" " + (strings.oneTimeDelayFromNow || "from now") +
1702+
" • " + nextRunDate.toLocaleString(locale);
15751703
}
15761704

15771705
function formatHistoryLabel(entry) {
@@ -3051,14 +3179,23 @@ syncTodoLabelSuggestions();
30513179
strings.boardCommentEditHint || "Add a focused update without rewriting the full description."
30523180
);
30533181
}
3054-
return comments.map(function (comment, commentIndex) {
3055-
var sourceLabel = getTodoCommentSourceLabel(comment.source || "human-form");
3182+
return comments.slice().reverse().map(function (comment, reverseIndex) {
3183+
var source = comment && comment.source ? String(comment.source) : "human-form";
3184+
var commentIndex = comments.length - reverseIndex - 1;
3185+
var sourceLabel = getTodoCommentSourceLabel(source);
30563186
var sequence = typeof comment.sequence === "number" ? comment.sequence : 1;
30573187
var displayDate = comment.updatedAt || comment.editedAt || comment.createdAt;
30583188
var toneClass = getTodoCommentToneClass(comment);
3059-
var userFormClass = comment.source === "human-form" && String(comment.author || "").toLowerCase() === "user"
3189+
var userFormClass = source === "human-form" && String(comment.author || "").toLowerCase() === "user"
30603190
? " is-user-form"
30613191
: "";
3192+
var rawBody = String(comment.body || "");
3193+
var previewBody = source === "system-event"
3194+
? rawBody.replace(/\s+/g, " ").trim()
3195+
: rawBody;
3196+
if (source === "system-event" && previewBody.length > 140) {
3197+
previewBody = previewBody.slice(0, 137) + "...";
3198+
}
30623199
return '<article class="todo-comment-card' + toneClass + userFormClass + '" data-comment-index="' + escapeAttr(String(commentIndex)) + '" tabindex="0" role="button" aria-label="' + escapeAttr(strings.boardCommentOpenFull || "Open full comment") + '">' +
30633200
'<div class="todo-comment-header">' +
30643201
'<div class="todo-comment-heading">' +
@@ -3071,7 +3208,7 @@ syncTodoLabelSuggestions();
30713208
'</div>' +
30723209
'</div>' +
30733210
'<div class="note todo-comment-author">' + escapeHtml(comment.author || "system") + '</div>' +
3074-
'<div class="todo-comment-body">' + escapeHtml(comment.body || "") + '</div>' +
3211+
'<div class="todo-comment-body">' + escapeHtml(previewBody) + '</div>' +
30753212
'<div class="todo-comment-expand-hint">' + escapeHtml(strings.boardCommentOpenFull || "Open full comment") + '</div>' +
30763213
'</article>';
30773214
}).join("");
@@ -4699,6 +4836,7 @@ syncTodoLabelSuggestions();
46994836
name: taskNameEl ? String(taskNameEl.value || "") : "",
47004837
prompt: promptTextEl ? String(promptTextEl.value || "") : "",
47014838
cronExpression: cronExpression ? String(cronExpression.value || "") : "",
4839+
oneTimeDelaySeconds: getOneTimeDelaySecondsFromInputs(),
47024840
labels: normalizeTaskLabelsValue(taskLabelsInput ? taskLabelsInput.value : ""),
47034841
agent: agentValue,
47044842
model: modelValue,
@@ -4720,6 +4858,7 @@ syncTodoLabelSuggestions();
47204858
name: String(task.name || ""),
47214859
prompt: typeof task.prompt === "string" ? task.prompt : "",
47224860
cronExpression: String(task.cronExpression || ""),
4861+
oneTimeDelaySeconds: deriveTaskOneTimeDelaySeconds(task),
47234862
labels: normalizeTaskLabelsValue(toLabelString(task.labels)),
47244863
agent: String(task.agent || ""),
47254864
model: String(task.model || ""),
@@ -5208,6 +5347,7 @@ syncTodoLabelSuggestions();
52085347
if (!isPersistedTabName(tabName)) {
52095348
tabName = "help";
52105349
}
5350+
var shouldRefreshStorageStatus = tabName === "settings" && activeTabName !== "settings";
52115351
if (activeTabName) {
52125352
captureTabScrollPosition(activeTabName);
52135353
}
@@ -5226,6 +5366,9 @@ syncTodoLabelSuggestions();
52265366
restoreTabScrollPosition(tabName);
52275367
updateBoardAutoCollapseFromScroll(true);
52285368
scheduleBoardStickyMetrics();
5369+
if (shouldRefreshStorageStatus) {
5370+
vscode.postMessage({ type: "refreshStorageStatus" });
5371+
}
52295372
maybePlayInitialHelpWarp(tabName);
52305373
}
52315374

@@ -5260,6 +5403,12 @@ syncTodoLabelSuggestions();
52605403
bindGenericChange(manualSessionToggle, function () {
52615404
syncRecurringChatSessionUi();
52625405
});
5406+
[oneTimeDelayHours, oneTimeDelayMinutes, oneTimeDelaySeconds].forEach(function (control) {
5407+
bindGenericChange(control, function () {
5408+
updateOneTimeDelayPreview();
5409+
syncEditorTabLabels();
5410+
});
5411+
});
52635412

52645413
bindTaskFilterBar(taskFilterBar, {
52655414
syncTaskFilterButtons: syncTaskFilterButtons,
@@ -5373,6 +5522,32 @@ syncTodoLabelSuggestions();
53735522
"jobs-friendly-frequency": function () {
53745523
refreshJobsFriendlyCronFromBuilder();
53755524
},
5525+
"one-time-delay-hours": function () {
5526+
updateOneTimeDelayPreview();
5527+
},
5528+
"one-time-delay-minutes": function () {
5529+
updateOneTimeDelayPreview();
5530+
},
5531+
"one-time-delay-seconds": function () {
5532+
updateOneTimeDelayPreview();
5533+
},
5534+
});
5535+
5536+
document.addEventListener("click", function (event) {
5537+
var target = event && event.target && event.target.nodeType === 3
5538+
? event.target.parentElement
5539+
: event.target;
5540+
if (!target || typeof target.closest !== "function") {
5541+
return;
5542+
}
5543+
var presetButton = target.closest(".one-time-delay-preset");
5544+
if (!presetButton) {
5545+
return;
5546+
}
5547+
event.preventDefault();
5548+
setOneTimeDelayInputs(presetButton.getAttribute("data-seconds"));
5549+
updateOneTimeDelayPreview();
5550+
syncEditorTabLabels();
53765551
});
53775552

53785553
bindFriendlyCronBuilderAutoUpdate({
@@ -5715,6 +5890,7 @@ syncTodoLabelSuggestions();
57155890
syncBundledAgents: syncBundledAgentsBtn,
57165891
openCopilotSettings: openCopilotSettingsBtn,
57175892
openExtensionSettings: openExtensionSettingsBtn,
5893+
refreshStorageStatus: refreshStorageStatusBtn,
57185894
importStorageFromJson: importStorageFromJsonBtn,
57195895
exportStorageToJson: exportStorageToJsonBtn,
57205896
});
@@ -7108,7 +7284,7 @@ syncTodoLabelSuggestions();
71087284
}
71097285

71107286
function refreshTaskEditorDerivedState() {
7111-
[syncRecurringChatSessionUi, updateFriendlyVisibility, updateCronPreview].forEach(function (refreshFn) {
7287+
[syncRecurringChatSessionUi, updateFriendlyVisibility, updateCronPreview, updateOneTimeDelayPreview].forEach(function (refreshFn) {
71127288
refreshFn();
71137289
});
71147290
}
@@ -7290,6 +7466,7 @@ syncTodoLabelSuggestions();
72907466
if (jitterSecondsInput) {
72917467
jitterSecondsInput.value = String(defaultJitterSeconds);
72927468
}
7469+
setOneTimeDelayInputs(0);
72937470
if (taskLabelsInput) {
72947471
taskLabelsInput.value = "";
72957472
}
@@ -7358,6 +7535,7 @@ syncTodoLabelSuggestions();
73587535
if (jitterSecondsInput) {
73597536
jitterSecondsInput.value = String(task.jitterSeconds ?? defaultJitterSeconds);
73607537
}
7538+
setOneTimeDelayInputs(deriveTaskOneTimeDelaySeconds(task));
73617539

73627540
var runFirstEl = document.getElementById("run-first");
73637541
if (runFirstEl) runFirstEl.checked = false;
@@ -8199,6 +8377,9 @@ syncTodoLabelSuggestions();
81998377
"#model-select",
82008378
"#template-select",
82018379
"#jitter-seconds",
8380+
"#one-time-delay-hours",
8381+
"#one-time-delay-minutes",
8382+
"#one-time-delay-seconds",
82028383
"#chat-session",
82038384
"#run-first",
82048385
"#one-time",
@@ -8793,6 +8974,7 @@ syncTodoLabelSuggestions();
87938974
case "updateStorageSettings":
87948975
storageSettings = normalizeStorageSettings(message.storageSettings, storageSettings);
87958976
renderStorageSettingsControls();
8977+
showStorageStatusRefreshNote();
87968978
break;
87978979
case "updateApprovalMode":
87988980
if (approvalModeSelect && message.approvalMode) {

media/cockpitWebviewDomRefs.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ export function createSchedulerWebviewDomRefs(document) {
1111
restoreHistoryBtn: document.getElementById("restore-history-btn"),
1212
autoShowStartupNote: document.getElementById("auto-show-startup-note"),
1313
friendlyBuilder: document.getElementById("friendly-builder"),
14+
recurringScheduleGroup: document.getElementById("recurring-schedule-group"),
15+
oneTimeDelayGroup: document.getElementById("one-time-delay-group"),
1416
cronPreset: document.getElementById("cron-preset"),
1517
cronExpression: document.getElementById("cron-expression"),
18+
oneTimeDelayHours: document.getElementById("one-time-delay-hours"),
19+
oneTimeDelayMinutes: document.getElementById("one-time-delay-minutes"),
20+
oneTimeDelaySeconds: document.getElementById("one-time-delay-seconds"),
21+
oneTimeDelayPreviewText: document.getElementById("one-time-delay-preview-text"),
1622
agentSelect: document.getElementById("agent-select"),
1723
modelSelect: document.getElementById("model-select"),
1824
chatSessionGroup: document.getElementById("chat-session-group"),
@@ -31,6 +37,8 @@ export function createSchedulerWebviewDomRefs(document) {
3137
syncBundledAgentsBtn: document.getElementById("sync-bundled-agents-btn"),
3238
openCopilotSettingsBtn: document.getElementById("open-copilot-settings-btn"),
3339
openExtensionSettingsBtn: document.getElementById("open-extension-settings-btn"),
40+
refreshStorageStatusBtn: document.getElementById("refresh-storage-status-btn"),
41+
settingsStatusRefreshNote: document.getElementById("settings-status-refresh-note"),
3442
importStorageFromJsonBtn: document.getElementById("import-storage-from-json-btn"),
3543
exportStorageToJsonBtn: document.getElementById("export-storage-to-json-btn"),
3644
helpLanguageSelect: document.getElementById("help-language-select"),
@@ -52,6 +60,7 @@ export function createSchedulerWebviewDomRefs(document) {
5260
taskFilterBar: document.getElementById("task-filter-bar"),
5361
taskLabelFilter: document.getElementById("task-label-filter"),
5462
taskLabelsInput: document.getElementById("task-labels"),
63+
runFirstGroup: document.getElementById("run-first-group"),
5564
jobsFolderList: document.getElementById("jobs-folder-list"),
5665
jobsCurrentFolderBanner: document.getElementById("jobs-current-folder-banner"),
5766
jobsList: document.getElementById("jobs-list"),

0 commit comments

Comments
 (0)