loopengine 0.2.1 → 0.3.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/README.md +1 -1
- package/dist/core/agent-config.d.ts +4 -0
- package/dist/core/model-calls/anthropic-model-call.d.ts +7 -0
- package/dist/core/model-calls/anthropic-model-call.js +34 -5
- package/dist/core/model-calls/openai-model-call.js +2 -1
- package/dist/core/run-agent.js +11 -2
- package/dist/core/scheduler.d.ts +29 -1
- package/dist/core/scheduler.js +28 -4
- package/dist/core/skillgarden/loader.d.ts +5 -1
- package/dist/core/skillgarden/loader.js +10 -3
- package/dist/core/skillgarden/substitute.d.ts +15 -1
- package/dist/core/skillgarden/substitute.js +28 -6
- package/dist/core/system-agents/templates.d.ts +5 -0
- package/dist/core/system-agents/workflow-planner.d.ts +26 -1
- package/dist/core/system-agents/workflow-planner.js +57 -5
- package/dist/core/system-tools/schedule.d.ts +5 -1
- package/dist/core/system-tools/schedule.js +45 -10
- package/dist/core/workflows.d.ts +52 -23
- package/dist/core/workflows.js +218 -27
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/web/agent-file-admin.d.ts +2 -0
- package/dist/web/agent-file-admin.js +8 -6
- package/dist/web/agents-config-page.js +411 -24
- package/dist/web/skills-admin.d.ts +4 -8
- package/dist/web/skills-admin.js +8 -12
- package/dist/web/templates-admin.js +7 -2
- package/dist/web/workflow-admin.d.ts +43 -18
- package/dist/web/workflow-admin.js +117 -50
- package/dist/web/workflow-page.js +72 -0
- package/package.json +1 -1
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { devUiCss } from './dev-ui-styles.js';
|
|
29
29
|
import { MESSAGING_ICONS } from './messaging-icons.js';
|
|
30
30
|
import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
|
|
31
|
+
import { REASONING_PROVIDERS } from './agent-file-admin.js';
|
|
31
32
|
export const agentsConfigPageHtml = `<!doctype html>
|
|
32
33
|
<html lang="en">
|
|
33
34
|
<head>
|
|
@@ -48,8 +49,20 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
48
49
|
form.add-source .schedule-when input[type="number"] { width: 80px; }
|
|
49
50
|
form.add-source .schedule-when select, form.add-source .schedule-when input[type="datetime-local"] { width: auto; }
|
|
50
51
|
.workflow-card { margin-bottom: 10px; }
|
|
51
|
-
.workflow-steps {
|
|
52
|
-
.workflow-
|
|
52
|
+
.workflow-steps-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; margin: 6px 0 0; padding: 8px 10px; border-radius: 6px; background: var(--le-surface, #f6f7f9); }
|
|
53
|
+
.workflow-params { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
54
|
+
.workflow-params[hidden] { display: none; }
|
|
55
|
+
.workflow-editor[hidden] { display: none; }
|
|
56
|
+
.workflow-hooks[hidden] { display: none; }
|
|
57
|
+
.hook-custom[hidden] { display: none; }
|
|
58
|
+
.workflow-hook { border-top: 1px solid var(--le-line, #e5e7eb); padding: 8px 0; }
|
|
59
|
+
.hook-url { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; margin: 6px 0; border-radius: 6px; background: var(--le-surface, #f6f7f9); font-size: 13px; }
|
|
60
|
+
.hook-url input { font-family: var(--le-mono, monospace); font-size: 12.5px; }
|
|
61
|
+
.workflow-param-rows { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
|
|
62
|
+
.workflow-param-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
63
|
+
.workflow-param-row .wp-name { width: 160px; font-family: var(--le-mono, monospace); }
|
|
64
|
+
.workflow-param-row .wp-desc { flex: 1; min-width: 200px; }
|
|
65
|
+
.workflow-param { display: flex; flex-direction: column; gap: 3px; font-size: 12px; min-width: 180px; }
|
|
53
66
|
.advanced-part { font-size: 17px; font-weight: 600; margin: 28px 0 12px; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
54
67
|
.advanced-part#advanced-tools { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
55
68
|
.approvals-nudge { border: 1px solid var(--warn); background: var(--warn-soft); color: var(--ink); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 24px; font-size: 13px; }
|
|
@@ -191,6 +204,9 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
191
204
|
max-width: 480px;
|
|
192
205
|
}
|
|
193
206
|
form.add-source label { font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
207
|
+
form.add-source label[hidden] { display: none; }
|
|
208
|
+
.schedule-notify { display: flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 12px; cursor: pointer; }
|
|
209
|
+
.schedule-notify input { width: auto; margin: 0; }
|
|
194
210
|
form.add-source input, form.add-source select, form.add-source textarea { width: 100%; }
|
|
195
211
|
form.add-source textarea { font-family: ui-monospace, monospace; min-height: 60px; }
|
|
196
212
|
/* Overrides form.add-source's shared max-width/textarea defaults for
|
|
@@ -358,6 +374,8 @@ ${consoleSidebarHtml}
|
|
|
358
374
|
${consoleShellScript}
|
|
359
375
|
<script>
|
|
360
376
|
(function () {
|
|
377
|
+
// The providers whose models take a reasoning effort (web/agent-file-admin.ts).
|
|
378
|
+
var REASONING_PROVIDERS = ${JSON.stringify(REASONING_PROVIDERS)};
|
|
361
379
|
var empty = document.getElementById('empty');
|
|
362
380
|
var detail = document.getElementById('detail');
|
|
363
381
|
var currentName = null;
|
|
@@ -675,7 +693,7 @@ ${consoleShellScript}
|
|
|
675
693
|
return workflows.map(function (w) {
|
|
676
694
|
var s = w.schedule;
|
|
677
695
|
var state = !s
|
|
678
|
-
?
|
|
696
|
+
? '<span class="hint">' + escapeHtml(w.trigger) + '</span>'
|
|
679
697
|
: s.waiting
|
|
680
698
|
? '<span class="le-pill ask">Waiting for a decision</span>'
|
|
681
699
|
: s.status === 'paused'
|
|
@@ -683,53 +701,337 @@ ${consoleShellScript}
|
|
|
683
701
|
: s.status === 'running'
|
|
684
702
|
? '<span class="le-pill accent">Running</span>'
|
|
685
703
|
: '<span class="le-pill good">On</span>' + (s.nextRunAt ? ' <span class="hint">next ' + escapeHtml(new Date(s.nextRunAt).toLocaleString()) + '</span>' : '');
|
|
686
|
-
var
|
|
687
|
-
|
|
688
|
-
|
|
704
|
+
var paramNames = Object.keys(w.parameters || {});
|
|
705
|
+
var params = paramNames.map(function (p) {
|
|
706
|
+
var spec = w.parameters[p];
|
|
707
|
+
return '<label class="workflow-param">' + escapeHtml(p) + (spec.required ? '' : ' <span class="hint">(optional)</span>') +
|
|
708
|
+
'<input name="' + escapeHtml(p) + '"' + (spec.required ? ' required' : '') + ' placeholder="' + escapeHtml(spec.description || '') + '"></label>';
|
|
709
|
+
}).join('');
|
|
710
|
+
var scheduleCount = (w.schedules || []).filter(function (x) { return x.repeating; }).length;
|
|
711
|
+
var runs = (w.recentRuns || []).map(function (r) {
|
|
712
|
+
var pill = r.status === 'done' ? '<span class="le-pill good">Done</span>'
|
|
713
|
+
: r.status === 'error' ? '<span class="le-pill bad">Failed</span>'
|
|
714
|
+
: r.status === 'awaiting_approval' ? '<span class="le-pill ask">Waiting for a decision</span>'
|
|
715
|
+
: '<span class="le-pill accent">Running</span>';
|
|
716
|
+
var values = r.params ? Object.keys(r.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(r.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '';
|
|
717
|
+
var outcome = r.status === 'error' ? (r.error || '') : (r.result || '');
|
|
718
|
+
return '<li>' + pill + ' <span class="hint">' + escapeHtml(new Date(r.started_at).toLocaleString()) + (r.source === 'api' ? ' ' + String.fromCharCode(183) + ' from an app' : r.source === 'schedule' ? ' ' + String.fromCharCode(183) + ' scheduled' + (r.task_name ? ' (' + escapeHtml(r.task_name) + ')' : '') : '') + '</span>' +
|
|
719
|
+
(values ? '<div class="hint">' + values + '</div>' : '') +
|
|
720
|
+
(outcome ? '<div class="schedule-result">' + escapeHtml(outcome) + '</div>' : '') + '</li>';
|
|
689
721
|
}).join('');
|
|
690
722
|
return '<div class="source workflow-card" data-workflow="' + escapeHtml(w.id) + '">' +
|
|
691
723
|
'<div style="display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:baseline">' +
|
|
692
|
-
'<strong>' + escapeHtml(w.
|
|
693
|
-
'<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.
|
|
694
|
-
'<
|
|
724
|
+
'<strong>' + escapeHtml(w.title) + '</strong><span>' + state + '</span></div>' +
|
|
725
|
+
'<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.description) + (s ? ' ' + String.fromCharCode(183) + ' ' + escapeHtml(w.trigger) : '') + '</p>' +
|
|
726
|
+
'<details><summary class="hint">Steps <span>(the ' + escapeHtml(w.id) + ' skill)</span></summary><div class="workflow-steps-text">' + escapeHtml(w.steps) + '</div></details>' +
|
|
727
|
+
(params ? '<div class="workflow-params">' + params + '</div>' : '') +
|
|
728
|
+
(runs ? '<details class="workflow-runs"' + ((w.recentRuns || []).some(function (r) { return r.status === 'running' || r.status === 'awaiting_approval'; }) ? ' open' : '') + '><summary class="hint">Recent runs (' + w.recentRuns.length + ')</summary><ul class="schedule-history">' + runs + '</ul></details>' : '') +
|
|
695
729
|
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">' +
|
|
696
730
|
'<button type="button" class="workflow-run">Run now</button>' +
|
|
697
|
-
(
|
|
731
|
+
(scheduleCount ? '<a href="?tab=schedules" data-tab="schedules" class="hint" style="align-self:center">' + (scheduleCount > 1 ? 'Schedules (' + scheduleCount + ')' : 'Schedule') + '</a>' : '') +
|
|
732
|
+
'<button type="button" class="workflow-edit">Edit</button>' +
|
|
733
|
+
'<button type="button" class="workflow-hooks-btn">Webhooks</button>' +
|
|
734
|
+
'<a href="?tab=skills" data-tab="skills" class="hint" style="align-self:center">Edit steps</a>' +
|
|
698
735
|
'<button type="button" class="delete-btn workflow-forget">Forget</button>' +
|
|
699
|
-
'</div><div class="hint workflow-result"></div></div>';
|
|
736
|
+
'</div><div class="hint workflow-result"></div><div class="workflow-editor" hidden></div><div class="workflow-hooks" hidden></div></div>';
|
|
700
737
|
}).join('') + '<p class="hint" style="margin:6px 0 0"><a href="/workflows/new">Describe another workflow</a></p>';
|
|
701
738
|
}
|
|
702
739
|
|
|
740
|
+
var workflowsRefreshTimer = null;
|
|
741
|
+
var workflowsById = {};
|
|
742
|
+
// Set while a workflow's editor is open, so the running-run refresh
|
|
743
|
+
// doesn't redraw the card under someone typing.
|
|
744
|
+
var workflowEditing = false;
|
|
745
|
+
|
|
703
746
|
function loadWorkflowsSection(name) {
|
|
704
747
|
var el = detail.querySelector('#workflowsContent');
|
|
705
748
|
if (!el) return;
|
|
749
|
+
if (workflowsRefreshTimer) { clearTimeout(workflowsRefreshTimer); workflowsRefreshTimer = null; }
|
|
706
750
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows').then(function (r) {
|
|
707
751
|
if (currentName !== name) return;
|
|
708
752
|
if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
|
|
709
|
-
|
|
753
|
+
var workflows = r.body.workflows || [];
|
|
754
|
+
workflowsById = {};
|
|
755
|
+
workflows.forEach(function (w) { workflowsById[w.id] = w; });
|
|
756
|
+
el.innerHTML = renderWorkflowsHtml(name, workflows);
|
|
710
757
|
var cards = el.querySelectorAll('.workflow-card');
|
|
711
758
|
for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
|
|
759
|
+
// Keep a running run's status current while the Overview is open.
|
|
760
|
+
var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
|
|
761
|
+
if (running && currentTab === 'overview') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
|
|
712
762
|
});
|
|
713
763
|
}
|
|
714
764
|
|
|
765
|
+
// ---- The workflow editor: how a workflow's runs start. Its steps are
|
|
766
|
+
// the skill body, edited on the Skills tab.
|
|
767
|
+
function workflowParamRowHtml(pname, spec) {
|
|
768
|
+
return '<div class="workflow-param-row">' +
|
|
769
|
+
'<input class="wp-name" aria-label="Parameter name" placeholder="order_number" value="' + escapeHtml(pname || '') + '">' +
|
|
770
|
+
'<input class="wp-desc" aria-label="What it is" placeholder="What it is, e.g. The order to check" value="' + escapeHtml((spec && spec.description) || '') + '">' +
|
|
771
|
+
'<label class="http-tool-inline-label"><input type="checkbox" class="wp-required"' + (spec && spec.required ? ' checked' : '') + '> Required</label>' +
|
|
772
|
+
'<button type="button" class="delete-btn wp-remove">Remove</button></div>';
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function workflowEditorHtml(w) {
|
|
776
|
+
var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
|
|
777
|
+
return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
|
|
778
|
+
'<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '"></label>' +
|
|
779
|
+
'<label>Start message <span class="hint">(what each run begins with; $name puts in a parameter's value)</span>' +
|
|
780
|
+
'<textarea name="start_message" maxlength="4000" required>' + escapeHtml(w.startMessage) + '</textarea></label>' +
|
|
781
|
+
'<label>When it runs <span class="hint">(in words, for people; the timing itself is its schedule)</span><input name="trigger" maxlength="200" value="' + escapeHtml(w.trigger) + '"></label>' +
|
|
782
|
+
'<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
|
|
783
|
+
'<div class="workflow-param-rows">' + rows + '</div>' +
|
|
784
|
+
'<button type="button" class="wp-add" style="align-self:flex-start">Add parameter</button></div>' +
|
|
785
|
+
'<p class="hint workflow-editor-check" style="margin:0"></p>' +
|
|
786
|
+
'<div style="display:flex;gap:8px"><button type="submit" class="primary">Save</button><button type="button" class="wp-cancel">Cancel</button></div>' +
|
|
787
|
+
'<div class="hint workflow-editor-result"></div></form>';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Placeholders used in the start message or steps that no parameter
|
|
791
|
+
// declares, and declared parameters nothing uses.
|
|
792
|
+
function workflowPlaceholderCheck(w, form) {
|
|
793
|
+
var used = {};
|
|
794
|
+
var pattern = new RegExp('[$]([a-z_][a-z0-9_]*)', 'g');
|
|
795
|
+
var text = form.querySelector('[name="start_message"]').value + ' ' + (w.steps || '');
|
|
796
|
+
var m;
|
|
797
|
+
while ((m = pattern.exec(text))) used[m[1]] = true;
|
|
798
|
+
var declared = {};
|
|
799
|
+
var names = form.querySelectorAll('.wp-name');
|
|
800
|
+
for (var i = 0; i < names.length; i++) if (names[i].value.trim()) declared[names[i].value.trim()] = true;
|
|
801
|
+
var notes = [];
|
|
802
|
+
var missing = Object.keys(used).filter(function (n) { return !declared[n]; });
|
|
803
|
+
var unused = Object.keys(declared).filter(function (n) { return !used[n]; });
|
|
804
|
+
if (missing.length) notes.push('Uses ' + missing.map(function (n) { return '$' + n; }).join(', ') + ', which ' + (missing.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't a parameter: add ' + (missing.length > 1 ? 'them' : 'it') + ', or the text stays as written.');
|
|
805
|
+
if (unused.length) notes.push(unused.join(', ') + ' ' + (unused.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't used in the start message or steps: its value is passed as an input.');
|
|
806
|
+
form.querySelector('.workflow-editor-check').textContent = notes.join(' ');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function openWorkflowEditor(name, card) {
|
|
810
|
+
var w = workflowsById[card.dataset.workflow];
|
|
811
|
+
var box = card.querySelector('.workflow-editor');
|
|
812
|
+
if (!w || !box) return;
|
|
813
|
+
workflowEditing = true;
|
|
814
|
+
box.innerHTML = workflowEditorHtml(w);
|
|
815
|
+
box.hidden = false;
|
|
816
|
+
var form = box.querySelector('form');
|
|
817
|
+
var rows = form.querySelector('.workflow-param-rows');
|
|
818
|
+
var result = form.querySelector('.workflow-editor-result');
|
|
819
|
+
function check() { workflowPlaceholderCheck(w, form); }
|
|
820
|
+
function close() { workflowEditing = false; box.hidden = true; box.innerHTML = ''; }
|
|
821
|
+
rows.addEventListener('click', function (ev) {
|
|
822
|
+
if (ev.target.classList.contains('wp-remove')) { ev.target.closest('.workflow-param-row').remove(); check(); }
|
|
823
|
+
});
|
|
824
|
+
form.addEventListener('input', check);
|
|
825
|
+
form.querySelector('.wp-add').addEventListener('click', function () {
|
|
826
|
+
rows.insertAdjacentHTML('beforeend', workflowParamRowHtml('', null));
|
|
827
|
+
rows.lastElementChild.querySelector('.wp-name').focus();
|
|
828
|
+
});
|
|
829
|
+
form.querySelector('.wp-cancel').addEventListener('click', close);
|
|
830
|
+
form.addEventListener('submit', function (ev) {
|
|
831
|
+
ev.preventDefault();
|
|
832
|
+
var parameters = {};
|
|
833
|
+
var problem = '';
|
|
834
|
+
var list = rows.querySelectorAll('.workflow-param-row');
|
|
835
|
+
for (var i = 0; i < list.length; i++) {
|
|
836
|
+
var pname = list[i].querySelector('.wp-name').value.trim();
|
|
837
|
+
if (!pname) continue;
|
|
838
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(pname)) { problem = 'Parameter "' + pname + '": use lowercase letters, digits and _, starting with a letter, e.g. order_number.'; break; }
|
|
839
|
+
if (parameters[pname]) { problem = 'Parameter "' + pname + '" is listed twice.'; break; }
|
|
840
|
+
parameters[pname] = { description: list[i].querySelector('.wp-desc').value.trim(), required: list[i].querySelector('.wp-required').checked };
|
|
841
|
+
}
|
|
842
|
+
if (problem) { result.textContent = problem; return; }
|
|
843
|
+
result.textContent = 'Saving' + String.fromCharCode(8230);
|
|
844
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
|
|
845
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
846
|
+
body: JSON.stringify({ title: form.title.value, start_message: form.start_message.value, trigger: form.trigger.value, parameters: parameters }),
|
|
847
|
+
}).then(function (r) {
|
|
848
|
+
if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
|
|
849
|
+
workflowEditing = false;
|
|
850
|
+
loadWorkflowsSection(name);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
check();
|
|
854
|
+
form.title.focus();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ---- A workflow's webhook triggers (core/workflow-triggers.ts): secret
|
|
858
|
+
// URLs an outside service posts its events to, to start a run.
|
|
859
|
+
var HOOK_OUTCOMES = {
|
|
860
|
+
started: ['good', 'Started a run'], filtered: ['', 'Skipped by the filter'], duplicate: ['', 'A retry, skipped'],
|
|
861
|
+
busy: ['', 'Already running'], rejected: ['bad', 'Signature check failed'], invalid: ['bad', 'Couldn' + String.fromCharCode(8217) + 't start'], error: ['bad', 'Failed'],
|
|
862
|
+
};
|
|
863
|
+
var HOOK_SENDERS = { stripe: 'Stripe', github: 'GitHub', shopify: 'Shopify', standard: 'Standard Webhooks' };
|
|
864
|
+
|
|
865
|
+
function hookRowHtml(t) {
|
|
866
|
+
var last = (t.deliveries || []).slice(0, 3).map(function (d) {
|
|
867
|
+
var o = HOOK_OUTCOMES[d.outcome] || ['', d.outcome];
|
|
868
|
+
return '<li><span class="le-pill ' + o[0] + '">' + escapeHtml(o[1]) + '</span> <span class="hint">' + escapeHtml(new Date(d.at).toLocaleString()) + '</span>' +
|
|
869
|
+
(d.detail ? '<div class="schedule-result">' + escapeHtml(d.detail) + '</div>' : '') + '</li>';
|
|
870
|
+
}).join('');
|
|
871
|
+
var rules = [];
|
|
872
|
+
if (t.verify) rules.push(t.verify.sender === 'custom'
|
|
873
|
+
? 'checks the signature in ' + escapeHtml(t.verify.header) + ' (' + escapeHtml(t.verify.algorithm || 'sha256') + ', ' + escapeHtml(t.verify.encoding || 'hex') + ')'
|
|
874
|
+
: 'checks the ' + escapeHtml(HOOK_SENDERS[t.verify.sender] || t.verify.sender) + ' signature');
|
|
875
|
+
if (t.filter) rules.push('only when ' + escapeHtml(t.filter.path) + ' is ' + t.filter.values.map(escapeHtml).join(' or '));
|
|
876
|
+
if (t.mapping) rules.push(Object.keys(t.mapping).map(function (p) { return escapeHtml(p) + ' ' + String.fromCharCode(8592) + ' ' + escapeHtml(t.mapping[p]); }).join(', '));
|
|
877
|
+
if (t.payload_param) rules.push('the whole payload goes to ' + escapeHtml(t.payload_param));
|
|
878
|
+
if (t.notify) rules.push('sends each result to you');
|
|
879
|
+
if (t.callback_url) rules.push('reports to ' + escapeHtml(t.callback_url));
|
|
880
|
+
return '<div class="workflow-hook" data-trigger="' + escapeHtml(t.trigger_id) + '">' +
|
|
881
|
+
'<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:baseline"><strong>' + escapeHtml(t.name) + '</strong>' +
|
|
882
|
+
'<code class="hint">/hooks/workflows/' + escapeHtml(t.token_hint) + '</code>' + (t.enabled ? '' : ' <span class="le-pill">Off</span>') + '</div>' +
|
|
883
|
+
(rules.length ? '<p class="hint" style="margin:2px 0">' + rules.join(' ' + String.fromCharCode(183) + ' ') + '</p>' : '') +
|
|
884
|
+
(last ? '<ul class="schedule-history">' + last + '</ul>' : '<p class="hint" style="margin:2px 0">No deliveries yet.</p>') +
|
|
885
|
+
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px">' +
|
|
886
|
+
'<button type="button" class="hook-toggle">' + (t.enabled ? 'Turn off' : 'Turn on') + '</button>' +
|
|
887
|
+
'<button type="button" class="hook-rotate">New URL</button>' +
|
|
888
|
+
'<button type="button" class="delete-btn hook-delete">Delete</button></div></div>';
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function hookFormHtml(w) {
|
|
892
|
+
var params = Object.keys(w.parameters || {});
|
|
893
|
+
return '<form class="add-source hook-form" style="margin-top:10px">' +
|
|
894
|
+
'<h4 style="margin:0">Add a webhook</h4>' +
|
|
895
|
+
'<label>Name<input name="name" maxlength="80" placeholder="Stripe disputes"></label>' +
|
|
896
|
+
'<label>Check the sender' + String.fromCharCode(8217) + 's signature <span class="hint">(the URL itself is secret; this also proves who sent it)</span>' +
|
|
897
|
+
'<select name="sender"><option value="">No, the URL is enough</option><option value="stripe">Stripe</option><option value="github">GitHub</option><option value="shopify">Shopify</option>' +
|
|
898
|
+
'<option value="standard">Standard Webhooks (Clerk, Resend, OpenAI, Svix)</option><option value="custom">Another service: set it up myself</option></select></label>' +
|
|
899
|
+
'<div class="hook-custom body-field" hidden>How it signs <span class="hint">(from its webhook docs: an HMAC of the request body, in a header)</span>' +
|
|
900
|
+
'<div class="workflow-param-row">' +
|
|
901
|
+
'<input name="sig_header" placeholder="Header, e.g. Linear-Signature" style="flex:1">' +
|
|
902
|
+
'<select name="sig_algorithm" aria-label="Hash"><option value="sha256">SHA-256</option><option value="sha1">SHA-1</option><option value="sha512">SHA-512</option></select>' +
|
|
903
|
+
'<select name="sig_encoding" aria-label="Written as"><option value="hex">hex</option><option value="base64">base64</option></select>' +
|
|
904
|
+
'<input name="sig_prefix" placeholder="Prefix, e.g. sha256= (if any)" style="width:180px">' +
|
|
905
|
+
'</div></div>' +
|
|
906
|
+
'<label class="hook-secret" hidden>Signing secret <span class="hint">(from the sender' + String.fromCharCode(8217) + 's webhook settings)</span><input name="secret" autocomplete="off"></label>' +
|
|
907
|
+
'<label>Run only when <span class="hint">(optional: a field in the payload, and the values that start a run)</span>' +
|
|
908
|
+
'<div class="workflow-param-row"><input name="filter_path" placeholder="type"><input name="filter_values" placeholder="charge.dispute.created, charge.dispute.updated" style="flex:1"></div></label>' +
|
|
909
|
+
(params.length
|
|
910
|
+
? '<div class="body-field">Parameters from the payload <span class="hint">(a dot path into its JSON, e.g. data.object.id)</span>' +
|
|
911
|
+
params.map(function (p) { return '<div class="workflow-param-row"><code class="wp-name">' + escapeHtml(p) + '</code><input name="map:' + escapeHtml(p) + '" placeholder="data.object.id" style="flex:1"></div>'; }).join('') + '</div>' +
|
|
912
|
+
'<label>Or give the whole payload to <select name="payload_param"><option value="">(none)</option>' + params.map(function (p) { return '<option>' + escapeHtml(p) + '</option>'; }).join('') + '</select></label>'
|
|
913
|
+
: '<p class="hint" style="margin:0">This workflow takes no parameters, so the payload isn' + String.fromCharCode(8217) + 't passed on. Add one in Edit to use it.</p>') +
|
|
914
|
+
'<label class="http-tool-inline-label"><input type="checkbox" name="notify"> Send each run' + String.fromCharCode(8217) + 's result to you through the agent' + String.fromCharCode(8217) + 's messaging app</label>' +
|
|
915
|
+
'<label>Post each run' + String.fromCharCode(8217) + 's result to <span class="hint">(optional, an https URL)</span><input name="callback_url" placeholder="https://example.com/hooks/loopengine"></label>' +
|
|
916
|
+
'<div style="display:flex;gap:8px"><button type="submit" class="primary">Make the URL</button></div>' +
|
|
917
|
+
'<div class="hint hook-form-result"></div></form>';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function hookUrlHtml(url) {
|
|
921
|
+
return '<div class="hook-url"><strong>Copy this URL now</strong> <span class="hint">(it' + String.fromCharCode(8217) + 's shown only once; make a new one if it' + String.fromCharCode(8217) + 's lost)</span>' +
|
|
922
|
+
'<input readonly value="' + escapeHtml(url) + '" onclick="this.select()"></div>';
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function openWorkflowHooks(name, card) {
|
|
926
|
+
var w = workflowsById[card.dataset.workflow];
|
|
927
|
+
var box = card.querySelector('.workflow-hooks');
|
|
928
|
+
if (!w || !box) return;
|
|
929
|
+
if (!box.hidden) { box.hidden = true; box.innerHTML = ''; workflowEditing = false; return; }
|
|
930
|
+
workflowEditing = true;
|
|
931
|
+
box.hidden = false;
|
|
932
|
+
var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id) + '/triggers';
|
|
933
|
+
function render(shownUrl) {
|
|
934
|
+
box.innerHTML = '<p class="hint" style="margin:0">Loading' + String.fromCharCode(8230) + '</p>';
|
|
935
|
+
fetchJsonRetryOnce(base).then(function (r) {
|
|
936
|
+
if (!r.ok) { box.innerHTML = '<p class="error">' + escapeHtml(r.body.error || 'Could not load webhooks') + '</p>'; return; }
|
|
937
|
+
var list = r.body.triggers || [];
|
|
938
|
+
box.innerHTML = '<h4 style="margin:10px 0 4px">Webhooks</h4>' +
|
|
939
|
+
'<p class="hint" style="margin:0 0 6px">A secret URL an outside service (Stripe, GitHub, a form, monitoring) posts its events to; each one starts a run.</p>' +
|
|
940
|
+
(shownUrl ? hookUrlHtml(shownUrl) : '') +
|
|
941
|
+
(list.length ? list.map(hookRowHtml).join('') : '') + hookFormHtml(w);
|
|
942
|
+
wire();
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function act(id, path, opts) {
|
|
946
|
+
return fetchJsonRetryOnce(base + '/' + encodeURIComponent(id) + path, opts).then(function (r) {
|
|
947
|
+
if (!r.ok) { alert(r.body.error || 'That didn' + String.fromCharCode(8217) + 't work'); return; }
|
|
948
|
+
render(r.body.url);
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function wire() {
|
|
952
|
+
var rows = box.querySelectorAll('.workflow-hook');
|
|
953
|
+
for (var i = 0; i < rows.length; i++) {
|
|
954
|
+
(function (row) {
|
|
955
|
+
var id = row.dataset.trigger;
|
|
956
|
+
row.querySelector('.hook-toggle').addEventListener('click', function (ev) {
|
|
957
|
+
act(id, '', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled: ev.currentTarget.textContent === 'Turn on' }) });
|
|
958
|
+
});
|
|
959
|
+
row.querySelector('.hook-rotate').addEventListener('click', function (ev) {
|
|
960
|
+
var btn = ev.currentTarget;
|
|
961
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again: the old URL stops working'; return; }
|
|
962
|
+
act(id, '/rotate', { method: 'POST' });
|
|
963
|
+
});
|
|
964
|
+
row.querySelector('.hook-delete').addEventListener('click', function (ev) {
|
|
965
|
+
var btn = ev.currentTarget;
|
|
966
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to delete'; return; }
|
|
967
|
+
act(id, '', { method: 'DELETE' });
|
|
968
|
+
});
|
|
969
|
+
})(rows[i]);
|
|
970
|
+
}
|
|
971
|
+
var form = box.querySelector('.hook-form');
|
|
972
|
+
var secretField = form.querySelector('.hook-secret');
|
|
973
|
+
var customField = form.querySelector('.hook-custom');
|
|
974
|
+
form.sender.addEventListener('change', function () {
|
|
975
|
+
secretField.hidden = !form.sender.value;
|
|
976
|
+
customField.hidden = form.sender.value !== 'custom';
|
|
977
|
+
});
|
|
978
|
+
form.addEventListener('submit', function (ev) {
|
|
979
|
+
ev.preventDefault();
|
|
980
|
+
var result = form.querySelector('.hook-form-result');
|
|
981
|
+
var body = { name: form.name.value };
|
|
982
|
+
if (form.sender.value) body.verify = { sender: form.sender.value, secret: form.secret.value };
|
|
983
|
+
if (form.sender.value === 'custom') {
|
|
984
|
+
body.verify.header = form.sig_header.value.trim();
|
|
985
|
+
body.verify.algorithm = form.sig_algorithm.value;
|
|
986
|
+
body.verify.encoding = form.sig_encoding.value;
|
|
987
|
+
if (form.sig_prefix.value.trim()) body.verify.prefix = form.sig_prefix.value.trim();
|
|
988
|
+
}
|
|
989
|
+
var values = form.filter_values.value.split(',').map(function (v) { return v.trim(); }).filter(Boolean);
|
|
990
|
+
if (form.filter_path.value.trim() || values.length) body.filter = { path: form.filter_path.value.trim(), values: values };
|
|
991
|
+
var mapping = {};
|
|
992
|
+
var inputs = form.querySelectorAll('input[name^="map:"]');
|
|
993
|
+
for (var j = 0; j < inputs.length; j++) if (inputs[j].value.trim()) mapping[inputs[j].name.slice(4)] = inputs[j].value.trim();
|
|
994
|
+
if (Object.keys(mapping).length) body.mapping = mapping;
|
|
995
|
+
if (form.payload_param && form.payload_param.value) body.payload_param = form.payload_param.value;
|
|
996
|
+
if (form.notify.checked) body.notify = true;
|
|
997
|
+
if (form.callback_url.value.trim()) body.callback_url = form.callback_url.value.trim();
|
|
998
|
+
result.textContent = 'Making it' + String.fromCharCode(8230);
|
|
999
|
+
fetchJsonRetryOnce(base, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) {
|
|
1000
|
+
if (!r.ok) { result.textContent = r.body.error || 'Could not make it'; return; }
|
|
1001
|
+
render(r.body.url);
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
render(null);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
715
1008
|
function wireWorkflowCard(name, card) {
|
|
716
1009
|
var id = card.dataset.workflow;
|
|
717
1010
|
var result = card.querySelector('.workflow-result');
|
|
718
1011
|
var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(id);
|
|
1012
|
+
card.querySelector('.workflow-edit').addEventListener('click', function () { openWorkflowEditor(name, card); });
|
|
1013
|
+
card.querySelector('.workflow-hooks-btn').addEventListener('click', function () { openWorkflowHooks(name, card); });
|
|
719
1014
|
card.querySelector('.workflow-run').addEventListener('click', function (ev) {
|
|
720
1015
|
var btn = ev.currentTarget;
|
|
1016
|
+
var params = {};
|
|
1017
|
+
var inputs = card.querySelectorAll('.workflow-params input');
|
|
1018
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
1019
|
+
var value = inputs[i].value.trim();
|
|
1020
|
+
if (!value && inputs[i].required) { result.textContent = 'Fill in ' + inputs[i].name + ' first.'; inputs[i].focus(); return; }
|
|
1021
|
+
if (value) params[inputs[i].name] = value;
|
|
1022
|
+
}
|
|
721
1023
|
btn.disabled = true;
|
|
722
1024
|
result.textContent = 'Starting' + String.fromCharCode(8230);
|
|
723
|
-
fetchJsonRetryOnce(base + '/run', { method: 'POST' }).then(function (r) {
|
|
1025
|
+
fetchJsonRetryOnce(base + '/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ params: params, source: 'console' }) }).then(function (r) {
|
|
724
1026
|
btn.disabled = false;
|
|
725
|
-
result.textContent = r.
|
|
726
|
-
|
|
727
|
-
|
|
1027
|
+
if (!r.ok) { result.textContent = r.body.error || 'Could not start it.'; return; }
|
|
1028
|
+
result.textContent = 'Started. Questions and approvals go to this agent' + String.fromCharCode(8217) + 's messaging app; the result shows under Recent runs.';
|
|
1029
|
+
setTimeout(function () { loadWorkflowsSection(name); }, 800);
|
|
728
1030
|
});
|
|
729
1031
|
});
|
|
730
1032
|
card.querySelector('.workflow-forget').addEventListener('click', function (ev) {
|
|
731
1033
|
var btn = ev.currentTarget;
|
|
732
|
-
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its
|
|
1034
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its schedules are cancelled; the skill stays)'; return; }
|
|
733
1035
|
fetchJsonRetryOnce(base, { method: 'DELETE' }).then(function () { loadWorkflowsSection(name); });
|
|
734
1036
|
});
|
|
735
1037
|
}
|
|
@@ -792,7 +1094,7 @@ ${consoleShellScript}
|
|
|
792
1094
|
'<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
|
|
793
1095
|
'<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
|
|
794
1096
|
'<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
|
|
795
|
-
'<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (cfg.model.provider
|
|
1097
|
+
'<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (REASONING_PROVIDERS.indexOf(cfg.model.provider) !== -1 ? '(model default)' : 'n/a')) + '</dd>' +
|
|
796
1098
|
'</dl>' +
|
|
797
1099
|
'<form class="add-source" id="modelForm" style="display:none">' +
|
|
798
1100
|
'<label>Provider' +
|
|
@@ -808,7 +1110,7 @@ ${consoleShellScript}
|
|
|
808
1110
|
'<label>Max tokens <span class="hint">(blank for provider default)</span>' +
|
|
809
1111
|
'<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
|
|
810
1112
|
'</label>' +
|
|
811
|
-
'<label>Reasoning effort <span class="hint">(
|
|
1113
|
+
'<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default)</span>' +
|
|
812
1114
|
'<select name="reasoningEffort">' +
|
|
813
1115
|
['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
|
|
814
1116
|
return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
|
|
@@ -918,7 +1220,19 @@ ${consoleShellScript}
|
|
|
918
1220
|
var modelForm = overviewPanel.querySelector('#modelForm');
|
|
919
1221
|
var modelError = overviewPanel.querySelector('#modelError');
|
|
920
1222
|
|
|
1223
|
+
// Only OpenAI and Anthropic models take a reasoning effort: the
|
|
1224
|
+
// field shows for those, and is cleared for any other provider so
|
|
1225
|
+
// it's never saved with one.
|
|
1226
|
+
var providerSelect = modelForm.querySelector('select[name="provider"]');
|
|
1227
|
+
var reasoningField = modelForm.querySelector('#reasoningEffortField');
|
|
1228
|
+
function syncReasoningField() {
|
|
1229
|
+
var supported = REASONING_PROVIDERS.indexOf(providerSelect.value) !== -1;
|
|
1230
|
+
reasoningField.style.display = supported ? '' : 'none';
|
|
1231
|
+
if (!supported) reasoningField.querySelector('select').value = '';
|
|
1232
|
+
}
|
|
1233
|
+
providerSelect.addEventListener('change', syncReasoningField);
|
|
921
1234
|
editModelBtn.addEventListener('click', function () {
|
|
1235
|
+
syncReasoningField();
|
|
922
1236
|
modelDisplay.style.display = 'none';
|
|
923
1237
|
editModelBtn.style.display = 'none';
|
|
924
1238
|
modelForm.style.display = '';
|
|
@@ -1760,6 +2074,9 @@ ${consoleShellScript}
|
|
|
1760
2074
|
// The workspace's time zone, from GET /agents/:name/schedules (an
|
|
1761
2075
|
// older project adapter doesn't send it: then the browser's own).
|
|
1762
2076
|
var schedulesTimeZone = null;
|
|
2077
|
+
// The agent's workflows, from the same response: a new schedule can run
|
|
2078
|
+
// one of them instead of a message.
|
|
2079
|
+
var schedulesWorkflows = [];
|
|
1763
2080
|
|
|
1764
2081
|
function fmtScheduleTime(iso) {
|
|
1765
2082
|
var d = new Date(iso);
|
|
@@ -1792,8 +2109,16 @@ ${consoleShellScript}
|
|
|
1792
2109
|
'<div class="schedule-result">' + escapeHtml(h.status === 'ok' ? (h.result || '') : (h.error || '')) + '</div></li>';
|
|
1793
2110
|
}).join('');
|
|
1794
2111
|
return '<tr>' +
|
|
1795
|
-
'<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
|
|
1796
|
-
|
|
2112
|
+
'<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
|
|
2113
|
+
(t.workflow
|
|
2114
|
+
? '<div><span class="le-pill accent">Workflow</span> ' + escapeHtml(t.workflow.title) + '</div><div class="schedule-result hint">' + escapeHtml(t.workflow.start_message) +
|
|
2115
|
+
(t.params ? '<br>' + Object.keys(t.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(t.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '') + '</div>'
|
|
2116
|
+
: t.workflow_missing
|
|
2117
|
+
? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
|
|
2118
|
+
: '<div class="schedule-result hint">' + escapeHtml(t.message) + '</div>') +
|
|
2119
|
+
(t.status !== 'cancelled' && t.status !== 'done'
|
|
2120
|
+
? '<label class="schedule-notify hint" title="Send each run's result through this agent's messaging connection (Messaging tab)"><input type="checkbox" class="schedule-notify-toggle" data-id="' + escapeHtml(t.task_id) + '"' + (t.notify ? ' checked' : '') + '> Send each result to you</label>'
|
|
2121
|
+
: (t.notify ? '<div class="hint">Sent each result to you</div>' : '')) + '</td>' +
|
|
1797
2122
|
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(fmtScheduleTime(t.next_run_at)) + '</div>' : '') + '</td>' +
|
|
1798
2123
|
'<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
|
|
1799
2124
|
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
|
|
@@ -1808,11 +2133,22 @@ ${consoleShellScript}
|
|
|
1808
2133
|
var list = tasks.length
|
|
1809
2134
|
? '<div class="le-table-wrap"><table><thead><tr><th>Task</th><th>When</th><th>Status</th><th>Last run</th><th></th></tr></thead><tbody>' + tasks.map(renderScheduleRow).join('') + '</tbody></table></div>'
|
|
1810
2135
|
: '<p class="hint">Nothing scheduled yet.</p>';
|
|
1811
|
-
return '<p class="muted" style="margin:0 0 20px">
|
|
2136
|
+
return '<p class="muted" style="margin:0 0 20px">Workflows and messages this agent runs on a timer, like a cron job. A workflow schedule starts with the workflow's start message as it is at each run. Each run works like an app calling it over HTTP: an approval goes wherever the Messaging tab sends it, and with nowhere set, a call that needs approval is denied. The agent can also schedule its own tasks when asked in Chat (“remind me tomorrow at 9”); they appear here too.</p>' +
|
|
1812
2137
|
'<section><h3>Scheduled (' + active.length + ' active)</h3>' + list + '</section>' +
|
|
1813
2138
|
'<section><h3>New schedule</h3>' +
|
|
1814
2139
|
'<form class="add-source" id="scheduleForm" style="max-width:560px">' +
|
|
1815
|
-
|
|
2140
|
+
(schedulesWorkflows.length
|
|
2141
|
+
? '<div class="body-field">Runs' +
|
|
2142
|
+
'<div class="schedule-when"><input type="radio" name="runs" value="workflow" id="runsWorkflow" checked><label for="runsWorkflow">A workflow</label>' +
|
|
2143
|
+
'<select name="workflow_id" aria-label="Workflow">' + schedulesWorkflows.map(function (w) {
|
|
2144
|
+
return '<option value="' + escapeHtml(w.id) + '">' + escapeHtml(w.title) + '</option>';
|
|
2145
|
+
}).join('') + '</select></div>' +
|
|
2146
|
+
'<div class="workflow-params" id="scheduleParams" style="margin:0 0 4px 22px"></div>' +
|
|
2147
|
+
'<p class="hint" style="margin:0 0 4px 22px">Each run starts with the workflow's start message as it is then, so changing the workflow changes what runs.</p>' +
|
|
2148
|
+
'<div class="schedule-when"><input type="radio" name="runs" value="message" id="runsMessage"><label for="runsMessage">A message</label></div>' +
|
|
2149
|
+
'</div>'
|
|
2150
|
+
: '') +
|
|
2151
|
+
'<label id="scheduleMessageField"' + (schedulesWorkflows.length ? ' hidden' : '') + '>Message <span class="hint">(what the agent receives each time, as if someone typed it)</span><textarea name="message" placeholder="Summarize yesterday's support tickets and post the summary."></textarea></label>' +
|
|
1816
2152
|
'<div class="body-field">When' +
|
|
1817
2153
|
'<div class="schedule-when"><input type="radio" name="when" value="every" id="whenEvery" checked><label for="whenEvery">Every</label>' +
|
|
1818
2154
|
'<input type="number" name="everyN" value="1" min="1" aria-label="How many">' +
|
|
@@ -1830,6 +2166,15 @@ ${consoleShellScript}
|
|
|
1830
2166
|
'</form></section>';
|
|
1831
2167
|
}
|
|
1832
2168
|
|
|
2169
|
+
// The New schedule form's parameter fields ("param:<name>"), filled ones only.
|
|
2170
|
+
function scheduleParams(data) {
|
|
2171
|
+
var params = {};
|
|
2172
|
+
data.forEach(function (value, key) {
|
|
2173
|
+
if (key.indexOf('param:') === 0 && String(value).trim()) params[key.slice(6)] = String(value).trim();
|
|
2174
|
+
});
|
|
2175
|
+
return params;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
1833
2178
|
// A date field's value ("2026-11-02T09:00") goes as it is, for the
|
|
1834
2179
|
// server to read in the workspace's time zone; with no workspace time
|
|
1835
2180
|
// zone known, it's the browser's.
|
|
@@ -1857,15 +2202,53 @@ ${consoleShellScript}
|
|
|
1857
2202
|
request.then(function () { setTimeout(function () { loadSchedulesTab(name); }, action === 'run' ? 800 : 0); });
|
|
1858
2203
|
});
|
|
1859
2204
|
}
|
|
2205
|
+
var notifyToggles = content.querySelectorAll('.schedule-notify-toggle');
|
|
2206
|
+
for (var n = 0; n < notifyToggles.length; n++) {
|
|
2207
|
+
notifyToggles[n].addEventListener('change', function (ev) {
|
|
2208
|
+
var box = ev.currentTarget;
|
|
2209
|
+
box.disabled = true;
|
|
2210
|
+
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(box.dataset.id) + '/notify', {
|
|
2211
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ notify: box.checked }),
|
|
2212
|
+
}).then(function (r) {
|
|
2213
|
+
box.disabled = false;
|
|
2214
|
+
if (!r.ok) { box.checked = !box.checked; alert('Could not change it: ' + (r.body.error || 'unknown error')); }
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
1860
2218
|
var toApprovals = content.querySelectorAll('.schedule-to-approvals');
|
|
1861
2219
|
for (var j = 0; j < toApprovals.length; j++) {
|
|
1862
2220
|
toApprovals[j].addEventListener('click', function (ev) { ev.preventDefault(); switchTab('messaging'); });
|
|
1863
2221
|
}
|
|
1864
2222
|
var form = content.querySelector('#scheduleForm');
|
|
1865
2223
|
var result = content.querySelector('#scheduleResult');
|
|
2224
|
+
var messageField = content.querySelector('#scheduleMessageField');
|
|
2225
|
+
var runsRadios = content.querySelectorAll('input[name="runs"]');
|
|
2226
|
+
var paramsBox = content.querySelector('#scheduleParams');
|
|
2227
|
+
var workflowSelect = form.querySelector('select[name="workflow_id"]');
|
|
2228
|
+
// One field per parameter of the chosen workflow.
|
|
2229
|
+
function renderScheduleParams() {
|
|
2230
|
+
if (!paramsBox || !workflowSelect) return;
|
|
2231
|
+
var w = schedulesWorkflows.filter(function (x) { return x.id === workflowSelect.value; })[0];
|
|
2232
|
+
var parameters = (w && w.parameters) || {};
|
|
2233
|
+
paramsBox.innerHTML = Object.keys(parameters).map(function (p) {
|
|
2234
|
+
return '<label class="workflow-param">' + escapeHtml(p) + (parameters[p].required ? '' : ' <span class="hint">(optional)</span>') +
|
|
2235
|
+
'<input name="param:' + escapeHtml(p) + '" placeholder="' + escapeHtml(parameters[p].description || '') + '"></label>';
|
|
2236
|
+
}).join('');
|
|
2237
|
+
}
|
|
2238
|
+
renderScheduleParams();
|
|
2239
|
+
if (workflowSelect) workflowSelect.addEventListener('change', renderScheduleParams);
|
|
2240
|
+
for (var k = 0; k < runsRadios.length; k++) {
|
|
2241
|
+
runsRadios[k].addEventListener('change', function () {
|
|
2242
|
+
var byMessage = form.querySelector('input[name="runs"]:checked').value === 'message';
|
|
2243
|
+
messageField.hidden = !byMessage;
|
|
2244
|
+
if (paramsBox) paramsBox.hidden = byMessage;
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
1866
2247
|
form.addEventListener('submit', function (ev) {
|
|
1867
2248
|
ev.preventDefault();
|
|
1868
2249
|
var data = new FormData(form);
|
|
2250
|
+
var runsWorkflow = data.get('runs') === 'workflow';
|
|
2251
|
+
if (!runsWorkflow && !String(data.get('message') || '').trim()) { result.textContent = 'Write the message the agent receives.'; return; }
|
|
1869
2252
|
var schedule;
|
|
1870
2253
|
if (data.get('when') === 'once') {
|
|
1871
2254
|
if (!data.get('runAt')) { result.textContent = 'Pick a date and time.'; return; }
|
|
@@ -1880,7 +2263,10 @@ ${consoleShellScript}
|
|
|
1880
2263
|
result.textContent = 'Scheduling' + String.fromCharCode(8230);
|
|
1881
2264
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/schedules', {
|
|
1882
2265
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1883
|
-
body: JSON.stringify(
|
|
2266
|
+
body: JSON.stringify(Object.assign(
|
|
2267
|
+
runsWorkflow ? { workflow_id: data.get('workflow_id'), params: scheduleParams(data) } : { message: data.get('message') },
|
|
2268
|
+
{ schedule: schedule, task_name: data.get('task_name'), notify: data.get('notify') === 'on' }
|
|
2269
|
+
)),
|
|
1884
2270
|
}).then(function (r) {
|
|
1885
2271
|
if (!r.ok) { result.textContent = r.body.error || 'Could not schedule'; return; }
|
|
1886
2272
|
loadSchedulesTab(name);
|
|
@@ -1897,6 +2283,7 @@ ${consoleShellScript}
|
|
|
1897
2283
|
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
1898
2284
|
var tasks = r.body.tasks || [];
|
|
1899
2285
|
schedulesTimeZone = r.body.time_zone || null;
|
|
2286
|
+
schedulesWorkflows = r.body.workflows || [];
|
|
1900
2287
|
content.innerHTML = renderSchedulesHtml(tasks);
|
|
1901
2288
|
wireSchedulesHandlers(name);
|
|
1902
2289
|
leShell.setCount('schedules', tasks.filter(function (t) { return t.status !== 'cancelled' && t.status !== 'done'; }).length);
|