@saltcorn/copilot 0.8.2 → 0.8.3
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/actions/generate-page.js +6 -2
- package/actions/generate-tables.js +56 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +155 -57
- package/agent-skills/registry-editor.js +15 -2
- package/app-constructor/common.js +7 -1
- package/app-constructor/errors.js +749 -61
- package/app-constructor/feedback-action.js +62 -60
- package/app-constructor/feedback.js +1294 -67
- package/app-constructor/fixed-prompts.js +829 -0
- package/app-constructor/phases.js +1485 -0
- package/app-constructor/prompt-generator.js +587 -0
- package/app-constructor/requirements.js +113 -54
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +195 -64
- package/app-constructor/schema.js +163 -324
- package/app-constructor/tasks.js +90 -886
- package/app-constructor/tools.js +13 -1
- package/app-constructor/view.js +307 -54
- package/builder-gen.js +11 -8
- package/builder-schema.js +6 -0
- package/package.json +1 -1
- package/app-constructor/prompts.js +0 -120
package/app-constructor/tasks.js
CHANGED
|
@@ -1,805 +1,46 @@
|
|
|
1
|
-
const Field = require("@saltcorn/data/models/field");
|
|
2
|
-
const Table = require("@saltcorn/data/models/table");
|
|
3
|
-
const Form = require("@saltcorn/data/models/form");
|
|
4
1
|
const MetaData = require("@saltcorn/data/models/metadata");
|
|
5
|
-
const
|
|
6
|
-
const Trigger = require("@saltcorn/data/models/trigger");
|
|
7
|
-
const Page = require("@saltcorn/data/models/page");
|
|
8
|
-
const Plugin = require("@saltcorn/data/models/plugin");
|
|
9
|
-
const { findType } = require("@saltcorn/data/models/discovery");
|
|
10
|
-
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
11
|
-
const db = require("@saltcorn/data/db");
|
|
12
|
-
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
13
|
-
const {
|
|
14
|
-
localeDateTime,
|
|
15
|
-
renderForm,
|
|
16
|
-
mkTable,
|
|
17
|
-
post_delete_btn,
|
|
18
|
-
} = require("@saltcorn/markup");
|
|
19
|
-
const {
|
|
20
|
-
div,
|
|
21
|
-
script,
|
|
22
|
-
domReady,
|
|
23
|
-
pre,
|
|
24
|
-
code,
|
|
25
|
-
input,
|
|
26
|
-
h4,
|
|
27
|
-
style,
|
|
28
|
-
h5,
|
|
29
|
-
button,
|
|
30
|
-
text_attr,
|
|
31
|
-
i,
|
|
32
|
-
p,
|
|
33
|
-
span,
|
|
34
|
-
small,
|
|
35
|
-
a,
|
|
36
|
-
textarea,
|
|
37
|
-
tr,
|
|
38
|
-
td,
|
|
39
|
-
} = require("@saltcorn/markup/tags");
|
|
40
|
-
const { getState } = require("@saltcorn/data/db/state");
|
|
41
|
-
const renderLayout = require("@saltcorn/markup/layout");
|
|
42
|
-
const { viewname } = require("./common");
|
|
2
|
+
const { div, button, text_attr, textarea } = require("@saltcorn/markup/tags");
|
|
43
3
|
const { runTask, runNextTask } = require("./run_task");
|
|
44
|
-
const { task_tool } = require("./tools");
|
|
45
|
-
const {
|
|
46
|
-
saltcorn_description,
|
|
47
|
-
existing_tables_list,
|
|
48
|
-
existing_entities_list,
|
|
49
|
-
available_plugins_list,
|
|
50
|
-
} = require("./prompts");
|
|
51
|
-
|
|
52
|
-
const doneTaskRowHtml = (task) =>
|
|
53
|
-
tr(
|
|
54
|
-
{ "data-row-id": task.id },
|
|
55
|
-
td(task.body.name || ""),
|
|
56
|
-
td(task.body.description || ""),
|
|
57
|
-
td((task.body.depends_on || []).join(", ")),
|
|
58
|
-
td(task.body.priority || ""),
|
|
59
|
-
td("Done"),
|
|
60
|
-
td(
|
|
61
|
-
task.body.run_id
|
|
62
|
-
? a(
|
|
63
|
-
{
|
|
64
|
-
target: "_blank",
|
|
65
|
-
href: `/view/Saltcorn%20Agent%20copilot?run_id=${task.body.run_id}`,
|
|
66
|
-
},
|
|
67
|
-
i({ class: "fas fa-external-link-alt" })
|
|
68
|
-
)
|
|
69
|
-
: ""
|
|
70
|
-
),
|
|
71
|
-
td(""),
|
|
72
|
-
td(
|
|
73
|
-
button(
|
|
74
|
-
{
|
|
75
|
-
class: "btn btn-outline-danger btn-sm",
|
|
76
|
-
onclick: `view_post("${viewname}", "del_task", {id:${task.id}})`,
|
|
77
|
-
},
|
|
78
|
-
i({ class: "fas fa-trash-alt" })
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const makeTaskList = async (req) => {
|
|
84
|
-
const rs = await MetaData.find(
|
|
85
|
-
{
|
|
86
|
-
type: "CopilotConstructMgr",
|
|
87
|
-
name: "task",
|
|
88
|
-
},
|
|
89
|
-
{ orderBy: "written_at" }
|
|
90
|
-
);
|
|
91
|
-
const settings = await MetaData.findOne({
|
|
92
|
-
type: "CopilotConstructMgr",
|
|
93
|
-
name: "settings",
|
|
94
|
-
});
|
|
95
|
-
const running = !!settings?.body?.running;
|
|
96
|
-
const stopping = !running && rs.some((t) => t.body.status === "Running");
|
|
97
|
-
const runningTask = rs.find((t) => t.body.status === "Running");
|
|
98
|
-
const statusText = runningTask
|
|
99
|
-
? span(
|
|
100
|
-
"Running: ",
|
|
101
|
-
span({ class: "fw-bold" }, runningTask.body.name || "task")
|
|
102
|
-
)
|
|
103
|
-
: running
|
|
104
|
-
? "Currently running"
|
|
105
|
-
: "Currently not running";
|
|
106
|
-
const status = div(
|
|
107
|
-
span({ id: "copilot-status-text" }, statusText),
|
|
108
|
-
running
|
|
109
|
-
? button(
|
|
110
|
-
{
|
|
111
|
-
class: "btn btn-danger ms-2",
|
|
112
|
-
onclick: `view_post("${viewname}", "stop", {})`,
|
|
113
|
-
},
|
|
114
|
-
i({ class: "fas fa-stop me-1" }),
|
|
115
|
-
"Stop running"
|
|
116
|
-
)
|
|
117
|
-
: button(
|
|
118
|
-
{
|
|
119
|
-
id: "copilot-start-btn",
|
|
120
|
-
class: "btn btn-success ms-2",
|
|
121
|
-
...(stopping
|
|
122
|
-
? { disabled: true }
|
|
123
|
-
: { onclick: `copilotStartRunning(this)` }),
|
|
124
|
-
},
|
|
125
|
-
stopping
|
|
126
|
-
? i({ class: "fas fa-spinner fa-spin me-1" })
|
|
127
|
-
: i({ class: "fas fa-play me-1" }),
|
|
128
|
-
stopping ? "Running..." : "Start running now"
|
|
129
|
-
),
|
|
130
|
-
stopping &&
|
|
131
|
-
span(
|
|
132
|
-
{
|
|
133
|
-
id: "copilot-stop-notice",
|
|
134
|
-
class:
|
|
135
|
-
"alert alert-warning alert-dismissible d-inline-block ms-2 py-1 px-2 mb-0",
|
|
136
|
-
style: "font-size:0.875rem",
|
|
137
|
-
},
|
|
138
|
-
button({
|
|
139
|
-
type: "button",
|
|
140
|
-
class: "btn-close btn-sm",
|
|
141
|
-
"data-bs-dismiss": "alert",
|
|
142
|
-
}),
|
|
143
|
-
"The current task will complete, then the queue stops. No new tasks will be started."
|
|
144
|
-
),
|
|
145
|
-
button(
|
|
146
|
-
{
|
|
147
|
-
id: "copilot-run-next-btn",
|
|
148
|
-
class: "btn btn-outline-success ms-2",
|
|
149
|
-
style: running || stopping ? "display:none" : "",
|
|
150
|
-
onclick: `copilotRunNext(this)`,
|
|
151
|
-
},
|
|
152
|
-
i({ class: "fas fa-play me-1" }),
|
|
153
|
-
"Run next task"
|
|
154
|
-
)
|
|
155
|
-
);
|
|
156
|
-
if (rs.length) {
|
|
157
|
-
const runningOnLoad =
|
|
158
|
-
!stopping && rs.some((t) => t.body.status === "Running");
|
|
159
|
-
return div(
|
|
160
|
-
{ class: "mt-2" },
|
|
161
|
-
status,
|
|
162
|
-
mkTable(
|
|
163
|
-
[
|
|
164
|
-
{ label: "Name", key: (m) => m.body.name },
|
|
165
|
-
{ label: "Description", key: (m) => m.body.description },
|
|
166
|
-
{
|
|
167
|
-
label: "Depends on",
|
|
168
|
-
key: (m) => (m.body.depends_on || []).join(", "),
|
|
169
|
-
},
|
|
170
|
-
{ label: "Priority", key: (m) => m.body.priority },
|
|
171
|
-
{ label: "Status", key: (m) => m.body.status || "To do" },
|
|
172
|
-
{
|
|
173
|
-
label: "Run",
|
|
174
|
-
key: (r) =>
|
|
175
|
-
r.body.status === "Running"
|
|
176
|
-
? span(
|
|
177
|
-
{
|
|
178
|
-
class: "task-spinner",
|
|
179
|
-
"data-task-id": r.id,
|
|
180
|
-
},
|
|
181
|
-
i({ class: "fas fa-spinner fa-spin text-warning" })
|
|
182
|
-
)
|
|
183
|
-
: r.body.status === "Done"
|
|
184
|
-
? r.body.run_id
|
|
185
|
-
? a(
|
|
186
|
-
{
|
|
187
|
-
target: "_blank",
|
|
188
|
-
href: `/view/Saltcorn%20Agent%20copilot?run_id=${r.body.run_id}`,
|
|
189
|
-
},
|
|
190
|
-
i({ class: "fas fa-external-link-alt" })
|
|
191
|
-
)
|
|
192
|
-
: ""
|
|
193
|
-
: button(
|
|
194
|
-
{
|
|
195
|
-
class: "btn btn-outline-success btn-sm",
|
|
196
|
-
"data-task-run": r.id,
|
|
197
|
-
onclick: `copilotRunTask(this,${r.id})`,
|
|
198
|
-
},
|
|
199
|
-
i({ class: "fas fa-play" })
|
|
200
|
-
),
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
label: "",
|
|
204
|
-
key: (r) =>
|
|
205
|
-
r.body.status === "To do" || !r.body.status
|
|
206
|
-
? button(
|
|
207
|
-
{
|
|
208
|
-
class: "btn btn-outline-secondary btn-sm",
|
|
209
|
-
title: "Mark as done without running",
|
|
210
|
-
onclick: `view_post("${viewname}", "mark_done_task", {id:${r.id}})`,
|
|
211
|
-
},
|
|
212
|
-
i({ class: "fas fa-check" })
|
|
213
|
-
)
|
|
214
|
-
: "",
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
label: "Delete",
|
|
218
|
-
key: (r) =>
|
|
219
|
-
button(
|
|
220
|
-
{
|
|
221
|
-
class: "btn btn-outline-danger btn-sm",
|
|
222
|
-
onclick: `view_post("${viewname}", "del_task", {id:${r.id}})`,
|
|
223
|
-
},
|
|
224
|
-
i({ class: "fas fa-trash-alt" })
|
|
225
|
-
),
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
{
|
|
229
|
-
"To do": rs.filter(
|
|
230
|
-
(t) =>
|
|
231
|
-
!t.body.status ||
|
|
232
|
-
t.body.status === "To do" ||
|
|
233
|
-
t.body.status === "Running"
|
|
234
|
-
),
|
|
235
|
-
Done: rs.filter((t) => t.body.status === "Done"),
|
|
236
|
-
},
|
|
237
|
-
{ grouped: true }
|
|
238
|
-
),
|
|
239
|
-
script(`
|
|
240
|
-
function copilotInitStopping() {
|
|
241
|
-
const startBtn = document.getElementById('copilot-start-btn');
|
|
242
|
-
const noticeEl = document.getElementById('copilot-stop-notice');
|
|
243
|
-
const runNextBtn = document.getElementById('copilot-run-next-btn');
|
|
244
|
-
const movedToDone = copilotDoneIds();
|
|
245
|
-
const poll = () => {
|
|
246
|
-
view_post(${JSON.stringify(viewname)}, 'tasks_poll', {}, (resp) => {
|
|
247
|
-
if (!resp || !resp.tasks) return;
|
|
248
|
-
let hasRunning = false;
|
|
249
|
-
for (const task of resp.tasks) {
|
|
250
|
-
if (task.status === 'Done' && !movedToDone.has(task.id)) {
|
|
251
|
-
movedToDone.add(task.id);
|
|
252
|
-
view_post(${JSON.stringify(
|
|
253
|
-
viewname
|
|
254
|
-
)}, 'task_row_done', {id: task.id}, (rowResp) => {
|
|
255
|
-
copilotAppendDoneRow(task.id, rowResp);
|
|
256
|
-
});
|
|
257
|
-
} else if (task.status === 'Running') {
|
|
258
|
-
hasRunning = true;
|
|
259
|
-
copilotShowSpinner(task.id);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (hasRunning) {
|
|
263
|
-
setTimeout(poll, 3000);
|
|
264
|
-
} else {
|
|
265
|
-
if (noticeEl) noticeEl.remove();
|
|
266
|
-
const statusTextEl = document.getElementById('copilot-status-text');
|
|
267
|
-
if (statusTextEl) statusTextEl.textContent = 'Currently not running';
|
|
268
|
-
if (startBtn) {
|
|
269
|
-
startBtn.disabled = false;
|
|
270
|
-
startBtn.innerHTML = '<i class="fas fa-play me-1"></i>Start running now';
|
|
271
|
-
startBtn.onclick = () => copilotStartRunning(startBtn);
|
|
272
|
-
}
|
|
273
|
-
if (runNextBtn) runNextBtn.style.display = '';
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
};
|
|
277
|
-
setTimeout(poll, 1000);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function copilotAppendDoneRow(taskId, rowResp) {
|
|
281
|
-
const row = document.querySelector('tr[data-row-id="' + taskId + '"]');
|
|
282
|
-
if (row) row.remove();
|
|
283
|
-
const doneHeader = Array.from(document.querySelectorAll('h4.list-group-header'))
|
|
284
|
-
.find(h => h.textContent.trim() === 'Done');
|
|
285
|
-
if (doneHeader && rowResp && rowResp.html) {
|
|
286
|
-
const tbody = doneHeader.closest('tr').parentNode;
|
|
287
|
-
const tmp = document.createElement('tbody');
|
|
288
|
-
tmp.innerHTML = rowResp.html;
|
|
289
|
-
tbody.appendChild(tmp.firstChild);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function copilotRunTask(btn, taskId) {
|
|
294
|
-
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
295
|
-
btn.disabled = true;
|
|
296
|
-
const runNextBtn = document.getElementById('copilot-run-next-btn');
|
|
297
|
-
const startBtn = document.getElementById('copilot-start-btn');
|
|
298
|
-
if (runNextBtn) runNextBtn.disabled = true;
|
|
299
|
-
if (startBtn) startBtn.disabled = true;
|
|
300
|
-
const row = document.querySelector('tr[data-row-id="' + taskId + '"]');
|
|
301
|
-
const taskName = row ? (row.cells[0]?.textContent?.trim() || '') : '';
|
|
302
|
-
const statusTextEl = document.getElementById('copilot-status-text');
|
|
303
|
-
if (statusTextEl) statusTextEl.innerHTML =
|
|
304
|
-
'Running: <span class="fw-bold">' + (taskName || 'task') + '</span>';
|
|
305
|
-
view_post(${JSON.stringify(viewname)}, 'run_task', {id: taskId}, () => {
|
|
306
|
-
const poll = () => {
|
|
307
|
-
view_post(${JSON.stringify(
|
|
308
|
-
viewname
|
|
309
|
-
)}, 'task_status', {ids: [String(taskId)]}, (resp) => {
|
|
310
|
-
if (resp && resp.any_done) {
|
|
311
|
-
if (statusTextEl) statusTextEl.textContent = 'Currently not running';
|
|
312
|
-
if (runNextBtn) runNextBtn.disabled = false;
|
|
313
|
-
if (startBtn) startBtn.disabled = false;
|
|
314
|
-
view_post(${JSON.stringify(
|
|
315
|
-
viewname
|
|
316
|
-
)}, 'task_row_done', {id: taskId}, (rowResp) => {
|
|
317
|
-
copilotAppendDoneRow(taskId, rowResp);
|
|
318
|
-
});
|
|
319
|
-
} else {
|
|
320
|
-
setTimeout(poll, 3000);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
setTimeout(poll, 3000);
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function copilotDoneIds() {
|
|
329
|
-
const ids = new Set();
|
|
330
|
-
const doneHeader = Array.from(document.querySelectorAll('h4.list-group-header'))
|
|
331
|
-
.find(h => h.textContent.trim() === 'Done');
|
|
332
|
-
if (doneHeader) {
|
|
333
|
-
let sib = doneHeader.closest('tr').nextElementSibling;
|
|
334
|
-
while (sib) {
|
|
335
|
-
const id = sib.getAttribute('data-row-id');
|
|
336
|
-
if (id) ids.add(Number(id));
|
|
337
|
-
sib = sib.nextElementSibling;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return ids;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function copilotShowSpinner(taskId) {
|
|
344
|
-
const row = document.querySelector('tr[data-row-id="' + taskId + '"]');
|
|
345
|
-
if (row && !row.querySelector('.task-spinner')) {
|
|
346
|
-
const runBtn = row.querySelector('[data-task-run]');
|
|
347
|
-
if (runBtn) {
|
|
348
|
-
runBtn.closest('td').innerHTML =
|
|
349
|
-
'<span class="task-spinner" data-task-id="' + taskId + '">' +
|
|
350
|
-
'<i class="fas fa-spinner fa-spin text-warning"></i></span>';
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function copilotRunNext(btn) {
|
|
356
|
-
btn.disabled = true;
|
|
357
|
-
const originalHtml = btn.innerHTML;
|
|
358
|
-
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Running...';
|
|
359
|
-
const firstRunBtn = document.querySelector('[data-task-run]');
|
|
360
|
-
const optimisticId = firstRunBtn ? Number(firstRunBtn.getAttribute('data-task-run')) : null;
|
|
361
|
-
if (optimisticId) copilotShowSpinner(optimisticId);
|
|
362
|
-
const movedToDone = copilotDoneIds();
|
|
363
|
-
view_post(${JSON.stringify(viewname)}, 'run_task', {}, () => {
|
|
364
|
-
const poll = () => {
|
|
365
|
-
view_post(${JSON.stringify(viewname)}, 'tasks_poll', {}, (resp) => {
|
|
366
|
-
if (!resp || !resp.tasks) return;
|
|
367
|
-
let hasRunning = false;
|
|
368
|
-
const runningIds = new Set(resp.tasks.filter(t => t.status === 'Running').map(t => t.id));
|
|
369
|
-
if (optimisticId && !runningIds.has(optimisticId) && !movedToDone.has(optimisticId)) {
|
|
370
|
-
const staleRow = document.querySelector('tr[data-row-id="' + optimisticId + '"]');
|
|
371
|
-
const staleSpinner = staleRow?.querySelector('.task-spinner');
|
|
372
|
-
if (staleSpinner) staleSpinner.closest('td').innerHTML =
|
|
373
|
-
'<button class="btn btn-outline-success btn-sm" data-task-run="' + optimisticId + '" onclick="copilotRunTask(this,' + optimisticId + ')"><i class="fas fa-play"></i></button>';
|
|
374
|
-
}
|
|
375
|
-
for (const task of resp.tasks) {
|
|
376
|
-
if (task.status === 'Done' && !movedToDone.has(task.id)) {
|
|
377
|
-
movedToDone.add(task.id);
|
|
378
|
-
view_post(${JSON.stringify(
|
|
379
|
-
viewname
|
|
380
|
-
)}, 'task_row_done', {id: task.id}, (rowResp) => {
|
|
381
|
-
copilotAppendDoneRow(task.id, rowResp);
|
|
382
|
-
});
|
|
383
|
-
} else if (task.status === 'Running') {
|
|
384
|
-
hasRunning = true;
|
|
385
|
-
copilotShowSpinner(task.id);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (hasRunning) setTimeout(poll, 3000);
|
|
389
|
-
else {
|
|
390
|
-
btn.disabled = false;
|
|
391
|
-
btn.innerHTML = originalHtml;
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
};
|
|
395
|
-
setTimeout(poll, 500);
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function copilotStartRunning(btn) {
|
|
400
|
-
btn.disabled = true;
|
|
401
|
-
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Running...';
|
|
402
|
-
const runNextBtn = document.getElementById('copilot-run-next-btn');
|
|
403
|
-
const statusTextEl = document.getElementById('copilot-status-text');
|
|
404
|
-
if (runNextBtn) runNextBtn.style.display = 'none';
|
|
405
|
-
if (statusTextEl) statusTextEl.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Running';
|
|
406
|
-
let stopped = false;
|
|
407
|
-
let stopNotice = null;
|
|
408
|
-
const stopBtn = document.createElement('button');
|
|
409
|
-
stopBtn.className = 'btn btn-danger ms-2';
|
|
410
|
-
stopBtn.innerHTML = '<i class="fas fa-stop me-1"></i>Stop';
|
|
411
|
-
stopBtn.onclick = () => {
|
|
412
|
-
stopped = true;
|
|
413
|
-
stopBtn.disabled = true;
|
|
414
|
-
view_post(${JSON.stringify(viewname)}, 'stop', {});
|
|
415
|
-
if (!stopNotice) {
|
|
416
|
-
stopNotice = document.createElement('span');
|
|
417
|
-
stopNotice.className = 'alert alert-warning alert-dismissible d-inline-block ms-2 py-1 px-2 mb-0';
|
|
418
|
-
stopNotice.style.fontSize = '0.875rem';
|
|
419
|
-
stopNotice.innerHTML =
|
|
420
|
-
'<button type="button" class="btn-close btn-sm" data-bs-dismiss="alert"></button>' +
|
|
421
|
-
'The current task will complete, then the queue stops. No new tasks will be started.';
|
|
422
|
-
stopBtn.insertAdjacentElement('afterend', stopNotice);
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
btn.insertAdjacentElement('afterend', stopBtn);
|
|
426
|
-
view_post(${JSON.stringify(viewname)}, 'start', {}, () => {
|
|
427
|
-
const movedToDone = copilotDoneIds();
|
|
428
|
-
const poll = () => {
|
|
429
|
-
view_post(${JSON.stringify(viewname)}, 'tasks_poll', {}, (resp) => {
|
|
430
|
-
if (!resp || !resp.tasks) return;
|
|
431
|
-
let hasPending = false;
|
|
432
|
-
let hasRunning = false;
|
|
433
|
-
for (const task of resp.tasks) {
|
|
434
|
-
if (task.status === 'Done' && !movedToDone.has(task.id)) {
|
|
435
|
-
movedToDone.add(task.id);
|
|
436
|
-
view_post(${JSON.stringify(
|
|
437
|
-
viewname
|
|
438
|
-
)}, 'task_row_done', {id: task.id}, (rowResp) => {
|
|
439
|
-
copilotAppendDoneRow(task.id, rowResp);
|
|
440
|
-
});
|
|
441
|
-
} else if (task.status === 'Running') {
|
|
442
|
-
hasRunning = true;
|
|
443
|
-
copilotShowSpinner(task.id);
|
|
444
|
-
if (statusTextEl) statusTextEl.innerHTML =
|
|
445
|
-
'Running: <span class="fw-bold">' + (task.name || 'task') + '</span>';
|
|
446
|
-
} else if (task.status !== 'Done') {
|
|
447
|
-
hasPending = true;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
if (hasRunning || (!stopped && hasPending)) {
|
|
451
|
-
setTimeout(poll, 3000);
|
|
452
|
-
} else {
|
|
453
|
-
stopBtn.remove();
|
|
454
|
-
if (stopNotice) { stopNotice.remove(); stopNotice = null; }
|
|
455
|
-
btn.disabled = false;
|
|
456
|
-
btn.innerHTML = '<i class="fas fa-play me-1"></i>Start running now';
|
|
457
|
-
if (statusTextEl) statusTextEl.textContent = 'Currently not running';
|
|
458
|
-
if (runNextBtn) runNextBtn.style.display = '';
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
};
|
|
462
|
-
setTimeout(poll, 1000);
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
`),
|
|
466
|
-
stopping
|
|
467
|
-
? script(domReady(`copilotInitStopping();`))
|
|
468
|
-
: runningOnLoad
|
|
469
|
-
? script(
|
|
470
|
-
domReady(`
|
|
471
|
-
const runNextBtn = document.getElementById('copilot-run-next-btn');
|
|
472
|
-
const startBtn = document.getElementById('copilot-start-btn');
|
|
473
|
-
if (runNextBtn) runNextBtn.disabled = true;
|
|
474
|
-
if (startBtn) startBtn.disabled = true;
|
|
475
|
-
const pollTasks = () => {
|
|
476
|
-
const spinners = document.querySelectorAll('.task-spinner[data-task-id]');
|
|
477
|
-
if (!spinners.length) return;
|
|
478
|
-
const ids = Array.from(spinners).map(el => el.getAttribute('data-task-id'));
|
|
479
|
-
view_post(${JSON.stringify(viewname)}, 'task_status', {ids}, (resp) => {
|
|
480
|
-
if (resp && resp.any_done) location.reload();
|
|
481
|
-
else setTimeout(pollTasks, 3000);
|
|
482
|
-
});
|
|
483
|
-
};
|
|
484
|
-
setTimeout(pollTasks, 3000);
|
|
485
|
-
`)
|
|
486
|
-
)
|
|
487
|
-
: "",
|
|
488
|
-
button(
|
|
489
|
-
{
|
|
490
|
-
class: "btn btn-outline-danger mb-4",
|
|
491
|
-
onclick: `view_post("${viewname}", "del_all_tasks")`,
|
|
492
|
-
},
|
|
493
|
-
"Delete all"
|
|
494
|
-
)
|
|
495
|
-
);
|
|
496
|
-
} else {
|
|
497
|
-
const planning = await MetaData.findOne({
|
|
498
|
-
type: "CopilotConstructMgr",
|
|
499
|
-
name: "planning",
|
|
500
|
-
});
|
|
501
|
-
if (planning) {
|
|
502
|
-
return div(
|
|
503
|
-
{ class: "mt-2" },
|
|
504
|
-
p(
|
|
505
|
-
i({ class: "fas fa-spinner fa-spin me-2" }),
|
|
506
|
-
"Planning tasks, please wait..."
|
|
507
|
-
),
|
|
508
|
-
script(
|
|
509
|
-
domReady(`
|
|
510
|
-
const poll = () => {
|
|
511
|
-
view_post(${JSON.stringify(viewname)}, 'planning_status', {}, (resp) => {
|
|
512
|
-
if (resp && !resp.planning) location.reload();
|
|
513
|
-
else setTimeout(poll, 3000);
|
|
514
|
-
});
|
|
515
|
-
};
|
|
516
|
-
setTimeout(poll, 3000);
|
|
517
|
-
`)
|
|
518
|
-
)
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
return div(
|
|
522
|
-
{ class: "mt-2" },
|
|
523
|
-
p("No tasks found"),
|
|
524
|
-
button(
|
|
525
|
-
{
|
|
526
|
-
class: "btn btn-primary",
|
|
527
|
-
onclick: `press_store_button(this);view_post("${viewname}", "gen_tasks")`,
|
|
528
|
-
},
|
|
529
|
-
"Plan tasks"
|
|
530
|
-
)
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const get_view_config_tool = {
|
|
536
|
-
type: "function",
|
|
537
|
-
function: {
|
|
538
|
-
name: "get_view_config",
|
|
539
|
-
description:
|
|
540
|
-
"Fetch the full configuration of an existing view so you can decide whether to reuse it or create a new one. Call this before planning a task if you are unsure whether an existing view already meets the requirements.",
|
|
541
|
-
parameters: {
|
|
542
|
-
type: "object",
|
|
543
|
-
required: ["name"],
|
|
544
|
-
properties: {
|
|
545
|
-
name: {
|
|
546
|
-
type: "string",
|
|
547
|
-
description: "The exact name of the existing view to inspect.",
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const doGenTasks = async (spec, rs, schema, userId) => {
|
|
555
|
-
const planningMd = await MetaData.create({
|
|
556
|
-
type: "CopilotConstructMgr",
|
|
557
|
-
name: "planning",
|
|
558
|
-
body: {},
|
|
559
|
-
});
|
|
560
|
-
try {
|
|
561
|
-
const tables = await Table.find({});
|
|
562
|
-
const tableById = Object.fromEntries(tables.map((t) => [t.id, t.name]));
|
|
563
|
-
const views = await View.find({});
|
|
564
|
-
const triggers = await Trigger.find({});
|
|
565
|
-
const pages = await Page.find({});
|
|
566
|
-
const entitiesSection = existing_entities_list({
|
|
567
|
-
views,
|
|
568
|
-
triggers,
|
|
569
|
-
pages,
|
|
570
|
-
tableById,
|
|
571
|
-
});
|
|
572
|
-
const installedPlugins = await Plugin.find({});
|
|
573
|
-
const installedNames = new Set(installedPlugins.map((p) => p.name));
|
|
574
|
-
let storePlugins = [];
|
|
575
|
-
try {
|
|
576
|
-
storePlugins = await Plugin.store_plugins_available();
|
|
577
|
-
} catch (_) {}
|
|
578
|
-
const pluginsSection = available_plugins_list(storePlugins, installedNames);
|
|
579
|
-
|
|
580
|
-
const systemPrompt =
|
|
581
|
-
"You are a project manager. The user wants to build an application, and you must analyse their application description";
|
|
582
|
-
|
|
583
|
-
const tools = [get_view_config_tool, task_tool];
|
|
584
|
-
const chat = [];
|
|
585
|
-
|
|
586
|
-
let answer = await getState().functions.llm_generate.run(
|
|
587
|
-
`Generate a plan for building this application:
|
|
588
|
-
|
|
589
|
-
Description: ${spec.body.description}
|
|
590
|
-
Audience: ${spec.body.audience}
|
|
591
|
-
Core features: ${spec.body.core_features}
|
|
592
|
-
Out of scope: ${spec.body.out_of_scope}
|
|
593
|
-
Visual style: ${spec.body.visual_style}
|
|
594
|
-
|
|
595
|
-
These are the requirements of the application:
|
|
596
|
-
|
|
597
|
-
${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
|
|
598
|
-
|
|
599
|
-
${saltcorn_description}
|
|
600
|
-
|
|
601
|
-
The database has already been built. The following tables are now present in the database:
|
|
602
|
-
|
|
603
|
-
${existing_tables_list(tables)}
|
|
604
|
-
|
|
605
|
-
The plan should outline the continued development of the application on top of this database.
|
|
606
|
-
Your plan can add additional tables if needed or adjust the table fields, but normally the tables
|
|
607
|
-
should be designed optimally for this application.
|
|
608
|
-
|
|
609
|
-
${entitiesSection ? entitiesSection + "\n\n" : ""}${
|
|
610
|
-
pluginsSection ? pluginsSection + "\n\n" : ""
|
|
611
|
-
}The plan should focus on building views, triggers (including workflows) and pages.
|
|
612
|
-
|
|
613
|
-
Important trigger planning rules:
|
|
614
|
-
* When a task involves a simple field update (e.g. marking an item complete or incomplete), plan it as a trigger using modify_row — NOT a workflow. Use a workflow only when multiple steps, branching, or looping are genuinely required.
|
|
615
|
-
* If multiple independent single-step actions are needed (e.g. "mark complete" and "mark incomplete"), describe them as separate triggers in the task description — do not describe them as one combined workflow.
|
|
616
|
-
* Do NOT mention "navigate back" or "return to context" in trigger task descriptions. Navigation is configured at the view level (GoBack button), not inside a trigger.
|
|
617
|
-
* If a trigger should be accessible as a button in a view, the task description must name the target view and say to add an action segment with action_name set to the trigger's name. If the view already exists, combine trigger creation and view update in the same task. If the view is created in a later task, that task's description must mention adding the trigger button, and it must depend on the trigger task.
|
|
618
|
-
* Do NOT plan any task that writes to a virtual (read-only) calculated field. Virtual fields are computed automatically and cannot be stored — any trigger or workflow that tries to update them will be refused. If you find yourself planning a trigger to keep a calculated field "current", delete that task — the field already updates itself.
|
|
619
|
-
|
|
620
|
-
Important view planning rules:
|
|
621
|
-
* Each task must create exactly one view. Never put two or more views in the same task. Edit, Show, and List for the same table are always three separate tasks with three separate names, descriptions, and dependencies.
|
|
622
|
-
* Do NOT plan separate tasks for "create" and "edit" on the same table. In Saltcorn, a single Edit view handles both (no id = create, id present = edit). One task, one Edit view, description says "create and edit".
|
|
623
|
-
* Edit, Show, and List views for a table always go together as three separate tasks. Whenever you plan a List view AND a Show view for the same table, you MUST also plan an Edit view for that table — a List without an Edit leaves users unable to create or modify records. Only omit the Edit view when the requirements explicitly say the data is read-only.
|
|
624
|
-
* The three tasks must be ordered: Edit and Show first (independent of each other, in any order), List last. The List task MUST list both the Edit task and the Show task in its depends_on — without exception. If you plan a List that depends on neither, that is a bug in the plan.
|
|
625
|
-
* Before finalising the plan, for every List view task, verify that its depends_on includes the corresponding Edit task and the corresponding Show task (if they exist). If either is missing, add it.
|
|
626
|
-
* When a List view links to a Show view or Edit view, the task description must say: "Add a viewlink column to [view_name] for the current row" — not just "link each row". This wording makes it unambiguous that a viewlink column must be added to the list for each target view.
|
|
627
|
-
* In general, if a view embeds or links to another view, the linked view's task must be listed as a dependency.
|
|
628
|
-
* When a table has foreign key fields referencing the users table, the task description must explicitly state for each one whether it is an ownership field (automatically set from the logged-in user, omit from the form) or a selector field (the user picks a value, include a selector in the form). Example: "user_id records the owner and is set automatically; shared_with_user_id must have a user selector."
|
|
629
|
-
* For FK fields that represent a parent context (e.g. trip_id on packing_items), always include the field as a normal selector in the Edit view form. Do NOT say to omit it. Saltcorn automatically pre-fills the selector from the URL query parameter when the view is opened from a parent context, and the user can select it manually when the view is used standalone.
|
|
630
|
-
* For every task that creates a view, include the exact view name in the task description. View names must be lowercase, snake_case, unique across all tasks in the plan, and descriptive enough to identify the table and purpose — for example 'packing_items_edit' rather than just 'edit'.
|
|
631
|
-
Important user account rules:
|
|
632
|
-
* The platform (Saltcorn) provides a built-in user account system with login, registration, and session management. Do NOT plan any tasks for user registration, login pages, password management, authentication flows, or email verification — these are already handled by the platform. Users register at /auth/signup and log in at /auth/login.
|
|
633
|
-
* User identity is always available as the logged-in user. Ownership fields (FK to users) are set automatically from the session; no custom logic is needed.
|
|
634
|
-
* If a requirement mentions "user accounts", "secure login", "saving data per user", "user-specific data", or "sharing between users", treat it as already satisfied by the platform's built-in user system. Do not generate any task in response to such a requirement.
|
|
635
|
-
|
|
636
|
-
Important role rules:
|
|
637
|
-
* Every view and page task description MUST state the min_role explicitly, e.g. "Set min_role to admin (1)." or "Set min_role to user (80).". Never omit it.
|
|
638
|
-
* Role values: admin=1, staff=40, user=80, public=100. Use the value that matches who will use the view or page — admin for management, staff for staff-only, user for logged-in users (clients, members, etc.), public only when the view or page must be accessible without login.
|
|
639
|
-
|
|
640
|
-
Important home page rules:
|
|
641
|
-
* Every role should land on the right page after visiting /. Plan a single task "Set home pages by role" that depends on all relevant page tasks and configures home_page_by_role for every role in one step.
|
|
642
|
-
* Role IDs: public=100, user=80, staff=40, admin=1.
|
|
643
|
-
* Landing/marketing page (public-facing intro): min_role must be 100 (public). It MUST include visible links to /auth/login (Log in) and /auth/signup (Create an account). Set as home for role 100 (public).
|
|
644
|
-
* If there is an admin dashboard page, set it as home for role 1 (admin).
|
|
645
|
-
* If there is a dashboard or main page for regular users or staff, set it as home for role 80 (user) and/or role 40 (staff) as appropriate.
|
|
646
|
-
* The "Set home pages by role" task description must list every role→page mapping explicitly using the exact page names planned in this task list, e.g.: "Set home_page_by_role: public (100) → landing, user (80) → client_dashboard, staff (40) → staff_dashboard, admin (1) → app_admin_dashboard." Never use "admin_dashboard" as a page name — it is reserved by the platform.
|
|
647
|
-
|
|
648
|
-
Important plugin rules:
|
|
649
|
-
* If multiple plugins need to be installed, combine them ALL into a single task named "Install plugins" that lists every required plugin name. Do NOT create a separate task per plugin.
|
|
650
|
-
|
|
651
|
-
Important dependency rules:
|
|
652
|
-
* Every name in a task's depends_on MUST exactly match the name field of another task in the same plan_tasks call. Never reference a name that is not present in the tasks array — not a concept, not a table name, not a made-up label. If you find yourself writing a depends_on entry whose name does not appear as a task name in the list, either add the missing task or remove the dependency.
|
|
653
|
-
* Before calling plan_tasks, mentally verify: for every task, every name in its depends_on array appears as the name of another task in the array.
|
|
654
|
-
|
|
655
|
-
Important schema/table rules:
|
|
656
|
-
* The database schema is already fully designed and implemented before task planning begins. ALL tables and fields needed by the application already exist. Do NOT plan any tasks that create tables, add fields, modify fields, or change the schema in any way. If you find yourself writing a task whose output is a table or a field, delete it — that work is already done.
|
|
657
|
-
* Ownership behaviour (auto-setting a FK-to-users field from the logged-in user) is configured in the Edit view, not in the database. Do not create tasks for it at the schema level.
|
|
658
|
-
* Do NOT plan tasks to add uniqueness constraints or validation to existing fields — those are already in the schema.
|
|
659
|
-
* Do NOT plan a standalone task for "access control", "row-level security", "permissions", or "roles". These are schema-level concerns already handled during schema design, or view-level concerns handled when building each view. The ownership field and sharing logic are already in the schema — there is nothing extra to configure as a separate task.
|
|
660
|
-
|
|
661
|
-
Your plan should not include any clarification or questions to the product owner. The
|
|
662
|
-
information you have been given so far is all that is available. Every step in the plan
|
|
663
|
-
should be immediately implementable in Saltcorn. You are writing the steps in the plan
|
|
664
|
-
for a person who is competent in using saltcorn but has no other business knowledge.
|
|
665
|
-
|
|
666
|
-
Do not include any steps that contain planning, design or review instructions. You are only writing a
|
|
667
|
-
plan for the engineer building the application. Every step in the plan should have the construction or the modification
|
|
668
|
-
of one or several application entity types.
|
|
669
|
-
|
|
670
|
-
Before finalising the plan, you may call get_view_config for any existing view you are unsure about — to inspect its configuration and decide whether a task should reuse it (updating it) or create a new one. Once you have gathered all necessary information, call plan_tasks to submit the complete task list.
|
|
671
|
-
`,
|
|
672
|
-
{
|
|
673
|
-
tools,
|
|
674
|
-
chat,
|
|
675
|
-
appendToChat: true,
|
|
676
|
-
systemPrompt,
|
|
677
|
-
}
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
const MAX_ITERATIONS = 10;
|
|
681
|
-
let iterations = 0;
|
|
682
|
-
|
|
683
|
-
while (iterations++ < MAX_ITERATIONS) {
|
|
684
|
-
if (typeof answer !== "object" || !answer.getToolCalls) break;
|
|
685
|
-
const toolCalls = answer.getToolCalls();
|
|
686
|
-
if (!toolCalls.length) break;
|
|
687
|
-
|
|
688
|
-
const planCall = toolCalls.find((tc) => tc.tool_name === "plan_tasks");
|
|
689
|
-
if (planCall) {
|
|
690
|
-
const tasks = planCall.input.tasks;
|
|
691
|
-
const plannedNames = new Set(tasks.map((t) => t.name).filter(Boolean));
|
|
692
|
-
|
|
693
|
-
for (const task of tasks) {
|
|
694
|
-
const validDeps = (task.depends_on || []).filter((nm) => {
|
|
695
|
-
if (!plannedNames.has(nm)) {
|
|
696
|
-
getState().log(
|
|
697
|
-
2,
|
|
698
|
-
`AppConstructor: dropping phantom dependency "${nm}" from task "${task.name}" — no such task in plan`
|
|
699
|
-
);
|
|
700
|
-
return false;
|
|
701
|
-
}
|
|
702
|
-
return true;
|
|
703
|
-
});
|
|
704
|
-
await MetaData.create({
|
|
705
|
-
type: "CopilotConstructMgr",
|
|
706
|
-
name: "task",
|
|
707
|
-
body: { ...task, depends_on: validDeps },
|
|
708
|
-
user_id: userId,
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
break;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const getViewCalls = toolCalls.filter(
|
|
715
|
-
(tc) => tc.tool_name === "get_view_config"
|
|
716
|
-
);
|
|
717
|
-
if (!getViewCalls.length) break;
|
|
718
|
-
|
|
719
|
-
for (const tc of getViewCalls) {
|
|
720
|
-
const viewName = tc.input?.name;
|
|
721
|
-
const view = viewName ? await View.findOne({ name: viewName }) : null;
|
|
722
|
-
const result = view
|
|
723
|
-
? JSON.stringify(
|
|
724
|
-
{
|
|
725
|
-
name: view.name,
|
|
726
|
-
viewtemplate: view.viewtemplate,
|
|
727
|
-
configuration: view.configuration,
|
|
728
|
-
},
|
|
729
|
-
null,
|
|
730
|
-
2
|
|
731
|
-
)
|
|
732
|
-
: `No view named "${viewName}" found.`;
|
|
733
|
-
|
|
734
|
-
if (answer.ai_sdk) {
|
|
735
|
-
chat.push({
|
|
736
|
-
role: "tool",
|
|
737
|
-
content: [
|
|
738
|
-
{
|
|
739
|
-
type: "tool-result",
|
|
740
|
-
toolCallId: tc.tool_call_id,
|
|
741
|
-
toolName: "get_view_config",
|
|
742
|
-
result,
|
|
743
|
-
},
|
|
744
|
-
],
|
|
745
|
-
});
|
|
746
|
-
} else {
|
|
747
|
-
chat.push({
|
|
748
|
-
role: "tool",
|
|
749
|
-
tool_call_id: tc.tool_call_id,
|
|
750
|
-
name: "get_view_config",
|
|
751
|
-
content: result,
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
answer = await getState().functions.llm_generate.run(null, {
|
|
757
|
-
tools,
|
|
758
|
-
chat,
|
|
759
|
-
appendToChat: true,
|
|
760
|
-
systemPrompt,
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
} finally {
|
|
764
|
-
await planningMd.delete();
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
const gen_tasks = async (table_id, viewname, config, body, { req, res }) => {
|
|
769
|
-
const spec = await MetaData.findOne({
|
|
770
|
-
type: "CopilotConstructMgr",
|
|
771
|
-
name: "spec",
|
|
772
|
-
});
|
|
773
|
-
if (!spec) throw new Error("Specification not found");
|
|
774
|
-
const rs = await MetaData.find({
|
|
775
|
-
type: "CopilotConstructMgr",
|
|
776
|
-
name: "requirement",
|
|
777
|
-
});
|
|
778
|
-
if (!rs.length) throw new Error("No requirements found");
|
|
779
|
-
const schema = await MetaData.findOne({
|
|
780
|
-
type: "CopilotConstructMgr",
|
|
781
|
-
name: "schema",
|
|
782
|
-
});
|
|
783
|
-
if (!schema) throw new Error("No schema found");
|
|
784
|
-
if (!schema.body.implemented) throw new Error("Schema not implemented");
|
|
785
|
-
doGenTasks(spec, rs, schema, req.user?.id).catch((e) =>
|
|
786
|
-
console.error("gen_tasks error", e)
|
|
787
|
-
);
|
|
788
|
-
return { json: { reload_page: true } };
|
|
789
|
-
};
|
|
790
4
|
|
|
791
5
|
const del_task = async (table_id, viewname, config, body, { req, res }) => {
|
|
792
6
|
const r = await MetaData.findOne({
|
|
793
7
|
id: body.id,
|
|
8
|
+
type: "CopilotConstructMgr",
|
|
9
|
+
name: "task",
|
|
794
10
|
});
|
|
795
11
|
|
|
796
12
|
if (!r) throw new Error("Task not found");
|
|
797
13
|
await r.delete();
|
|
798
|
-
return {
|
|
14
|
+
return {
|
|
15
|
+
json: {
|
|
16
|
+
eval_js:
|
|
17
|
+
"if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
799
20
|
};
|
|
800
21
|
const run_task = async (table_id, viewname, config, body, { req, res }) => {
|
|
801
22
|
const reqUser = req?.user;
|
|
802
23
|
if (body.id) {
|
|
24
|
+
if (!body.force) {
|
|
25
|
+
const task = await MetaData.findOne({ id: Number(body.id) });
|
|
26
|
+
const deps = task?.body?.depends_on || [];
|
|
27
|
+
if (deps.length > 0) {
|
|
28
|
+
const allTasks = await MetaData.find({
|
|
29
|
+
type: "CopilotConstructMgr",
|
|
30
|
+
name: "task",
|
|
31
|
+
});
|
|
32
|
+
const doneNames = new Set(
|
|
33
|
+
allTasks
|
|
34
|
+
.filter((t) => t.body.status === "Done")
|
|
35
|
+
.map((t) => t.body.name)
|
|
36
|
+
);
|
|
37
|
+
const unmet = deps.filter((d) => !doneNames.has(d));
|
|
38
|
+
if (unmet.length > 0)
|
|
39
|
+
return {
|
|
40
|
+
json: { unmet_deps: unmet, task_name: task.body.name || "" },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
803
44
|
runTask(body.id, { user: reqUser, __: req.__ }).catch((e) =>
|
|
804
45
|
console.error("run_task error", e)
|
|
805
46
|
);
|
|
@@ -809,47 +50,16 @@ const run_task = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
809
50
|
return { json: { success: true } };
|
|
810
51
|
};
|
|
811
52
|
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
config,
|
|
816
|
-
body,
|
|
817
|
-
{ req, res }
|
|
818
|
-
) => {
|
|
819
|
-
const planning = await MetaData.findOne({
|
|
820
|
-
type: "CopilotConstructMgr",
|
|
821
|
-
name: "planning",
|
|
822
|
-
});
|
|
823
|
-
return { json: { planning: !!planning } };
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
const tasks_poll = async (table_id, viewname, config, body, { req, res }) => {
|
|
827
|
-
const tasks = await MetaData.find({
|
|
53
|
+
const reset_task = async (table_id, viewname, config, body, { req, res }) => {
|
|
54
|
+
const r = await MetaData.findOne({
|
|
55
|
+
id: body.id,
|
|
828
56
|
type: "CopilotConstructMgr",
|
|
829
57
|
name: "task",
|
|
830
58
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
name: t.body.name,
|
|
836
|
-
status: t.body.status || "To do",
|
|
837
|
-
run_id: t.body.run_id,
|
|
838
|
-
})),
|
|
839
|
-
},
|
|
840
|
-
};
|
|
841
|
-
};
|
|
842
|
-
|
|
843
|
-
const task_row_done = async (
|
|
844
|
-
table_id,
|
|
845
|
-
viewname,
|
|
846
|
-
config,
|
|
847
|
-
body,
|
|
848
|
-
{ req, res }
|
|
849
|
-
) => {
|
|
850
|
-
const task = await MetaData.findOne({ id: body.id });
|
|
851
|
-
if (!task) return { json: { html: "" } };
|
|
852
|
-
return { json: { html: doneTaskRowHtml(task) } };
|
|
59
|
+
if (!r) throw new Error("Task not found");
|
|
60
|
+
const { status, run_id, ...rest } = r.body;
|
|
61
|
+
await r.update({ body: { ...rest, status: "To do" } });
|
|
62
|
+
return { json: { success: true } };
|
|
853
63
|
};
|
|
854
64
|
|
|
855
65
|
const task_status = async (table_id, viewname, config, body, { req, res }) => {
|
|
@@ -863,78 +73,72 @@ const task_status = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
863
73
|
return { json: { any_done } };
|
|
864
74
|
};
|
|
865
75
|
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
});
|
|
871
|
-
if (settings)
|
|
872
|
-
await settings.update({ body: { ...settings.body, running: true } });
|
|
873
|
-
else
|
|
874
|
-
await MetaData.create({
|
|
875
|
-
type: "CopilotConstructMgr",
|
|
876
|
-
name: "settings",
|
|
877
|
-
body: { running: true },
|
|
878
|
-
});
|
|
879
|
-
runNextTask().catch((e) => console.error("start error", e));
|
|
880
|
-
return { json: { success: true } };
|
|
881
|
-
};
|
|
882
|
-
const stop = async (table_id, viewname, config, body, { req, res }) => {
|
|
883
|
-
const settings = await MetaData.findOne({
|
|
76
|
+
const edit_task_desc = async (table_id, vname, config, body, { req, res }) => {
|
|
77
|
+
const id = body.id || req.query?.id;
|
|
78
|
+
const r = await MetaData.findOne({
|
|
79
|
+
id: Number(id),
|
|
884
80
|
type: "CopilotConstructMgr",
|
|
885
|
-
name: "
|
|
81
|
+
name: "task",
|
|
886
82
|
});
|
|
887
|
-
if (
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
83
|
+
if (!r) return { json: { error: "Task not found" } };
|
|
84
|
+
const html =
|
|
85
|
+
div(
|
|
86
|
+
{ class: "mb-3" },
|
|
87
|
+
textarea(
|
|
88
|
+
{
|
|
89
|
+
id: "edit-task-desc-text",
|
|
90
|
+
class: "form-control",
|
|
91
|
+
rows: "10",
|
|
92
|
+
},
|
|
93
|
+
text_attr(r.body.description || "")
|
|
94
|
+
)
|
|
95
|
+
) +
|
|
96
|
+
div(
|
|
97
|
+
{ class: "d-flex gap-2 mt-3" },
|
|
98
|
+
button(
|
|
99
|
+
{
|
|
100
|
+
type: "button",
|
|
101
|
+
class: "btn btn-primary",
|
|
102
|
+
onclick: `view_post(${JSON.stringify(vname)}, 'save_task_desc', {
|
|
103
|
+
id: ${r.id},
|
|
104
|
+
description: document.getElementById('edit-task-desc-text').value
|
|
105
|
+
}, () => {
|
|
106
|
+
$('#scmodal').modal('hide');
|
|
107
|
+
if (typeof copilotRefreshTasks === 'function') copilotRefreshTasks();
|
|
108
|
+
})`,
|
|
109
|
+
},
|
|
110
|
+
"Save"
|
|
111
|
+
),
|
|
112
|
+
button(
|
|
113
|
+
{
|
|
114
|
+
type: "button",
|
|
115
|
+
class: "btn btn-secondary",
|
|
116
|
+
"data-bs-dismiss": "modal",
|
|
117
|
+
},
|
|
118
|
+
"Cancel"
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
return { html, title: `Edit: ${r.body.name || "task"}` };
|
|
909
122
|
};
|
|
910
123
|
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
config,
|
|
915
|
-
body,
|
|
916
|
-
{ req, res }
|
|
917
|
-
) => {
|
|
918
|
-
const rs = await MetaData.find({
|
|
124
|
+
const save_task_desc = async (table_id, vname, config, body, { req, res }) => {
|
|
125
|
+
const r = await MetaData.findOne({
|
|
126
|
+
id: Number(body.id),
|
|
919
127
|
type: "CopilotConstructMgr",
|
|
920
128
|
name: "task",
|
|
921
129
|
});
|
|
922
|
-
|
|
923
|
-
|
|
130
|
+
if (!r) throw new Error("Task not found");
|
|
131
|
+
await r.update({ body: { ...r.body, description: body.description } });
|
|
132
|
+
return { json: { success: true } };
|
|
924
133
|
};
|
|
925
134
|
|
|
926
135
|
const task_routes = {
|
|
927
|
-
gen_tasks,
|
|
928
136
|
del_task,
|
|
929
|
-
|
|
930
|
-
|
|
137
|
+
reset_task,
|
|
138
|
+
edit_task_desc,
|
|
139
|
+
save_task_desc,
|
|
931
140
|
run_task,
|
|
932
|
-
planning_status,
|
|
933
141
|
task_status,
|
|
934
|
-
tasks_poll,
|
|
935
|
-
task_row_done,
|
|
936
|
-
start,
|
|
937
|
-
stop,
|
|
938
142
|
};
|
|
939
143
|
|
|
940
|
-
module.exports = {
|
|
144
|
+
module.exports = { task_routes };
|