bosun 0.36.2 → 0.36.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/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/codex-shell.mjs +85 -10
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +13 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +19 -4
- package/ui/components/chat-view.js +108 -5
- package/ui/components/session-list.js +1 -1
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice.js +15 -6
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +84 -12
- package/ui/tabs/chat.js +5 -1
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +193 -19
- package/update-check.mjs +41 -13
- package/voice-relay.mjs +816 -0
- package/voice-tools.mjs +679 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/ui/tabs/tasks.js
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
scheduleRefresh,
|
|
35
35
|
loadTasks,
|
|
36
36
|
updateTaskManualState,
|
|
37
|
+
setPendingChange,
|
|
38
|
+
clearPendingChange,
|
|
37
39
|
} from "../modules/state.js";
|
|
38
40
|
import { ICONS } from "../modules/icons.js";
|
|
39
41
|
import {
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
debounce,
|
|
45
47
|
exportAsCSV,
|
|
46
48
|
exportAsJSON,
|
|
49
|
+
countChangedFields,
|
|
47
50
|
} from "../modules/utils.js";
|
|
48
51
|
import {
|
|
49
52
|
Card,
|
|
@@ -53,6 +56,7 @@ import {
|
|
|
53
56
|
Modal,
|
|
54
57
|
EmptyState,
|
|
55
58
|
ListItem,
|
|
59
|
+
SaveDiscardBar,
|
|
56
60
|
} from "../components/shared.js";
|
|
57
61
|
import { SegmentedControl, SearchInput, Toggle } from "../components/forms.js";
|
|
58
62
|
import { KanbanBoard } from "../components/kanban-board.js";
|
|
@@ -255,6 +259,11 @@ function isImageAttachment(att) {
|
|
|
255
259
|
return /\.(png|jpe?g|gif|webp|bmp|svg)$/.test(name);
|
|
256
260
|
}
|
|
257
261
|
|
|
262
|
+
function unsavedChangesMessage(changeCount) {
|
|
263
|
+
const count = Math.max(0, Number(changeCount || 0));
|
|
264
|
+
return `You have unsaved changes (${count})`;
|
|
265
|
+
}
|
|
266
|
+
|
|
258
267
|
export function StartTaskModal({
|
|
259
268
|
task,
|
|
260
269
|
defaultSdk = "auto",
|
|
@@ -266,14 +275,27 @@ export function StartTaskModal({
|
|
|
266
275
|
const [model, setModel] = useState("");
|
|
267
276
|
const [taskIdInput, setTaskIdInput] = useState(task?.id || "");
|
|
268
277
|
const [starting, setStarting] = useState(false);
|
|
278
|
+
const initialSnapshotRef = useRef({
|
|
279
|
+
sdk: defaultSdk || "auto",
|
|
280
|
+
model: "",
|
|
281
|
+
taskIdInput: task?.id || "",
|
|
282
|
+
});
|
|
283
|
+
const pendingKey = useMemo(
|
|
284
|
+
() => `modal:start-task:${task?.id || "manual"}`,
|
|
285
|
+
[task?.id],
|
|
286
|
+
);
|
|
269
287
|
|
|
270
288
|
useEffect(() => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
289
|
+
const next = {
|
|
290
|
+
sdk: defaultSdk || "auto",
|
|
291
|
+
model: "",
|
|
292
|
+
taskIdInput: task?.id || "",
|
|
293
|
+
};
|
|
294
|
+
initialSnapshotRef.current = next;
|
|
295
|
+
setSdk(next.sdk);
|
|
296
|
+
setModel(next.model);
|
|
297
|
+
setTaskIdInput(next.taskIdInput);
|
|
298
|
+
}, [defaultSdk, task?.id, task?.meta?.codex?.isIgnored, task?.meta?.labels]);
|
|
277
299
|
|
|
278
300
|
const canModel = sdk && sdk !== "auto";
|
|
279
301
|
|
|
@@ -298,12 +320,38 @@ export function StartTaskModal({
|
|
|
298
320
|
};
|
|
299
321
|
|
|
300
322
|
const resolvedTaskId = (task?.id || taskIdInput || "").trim();
|
|
323
|
+
const currentSnapshot = useMemo(
|
|
324
|
+
() => ({
|
|
325
|
+
sdk: sdk || "auto",
|
|
326
|
+
model: model || "",
|
|
327
|
+
taskIdInput: taskIdInput || "",
|
|
328
|
+
}),
|
|
329
|
+
[model, sdk, taskIdInput],
|
|
330
|
+
);
|
|
331
|
+
const changeCount = useMemo(
|
|
332
|
+
() => countChangedFields(initialSnapshotRef.current, currentSnapshot),
|
|
333
|
+
[currentSnapshot],
|
|
334
|
+
);
|
|
335
|
+
const hasUnsaved = changeCount > 0;
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
setPendingChange(pendingKey, hasUnsaved);
|
|
339
|
+
return () => clearPendingChange(pendingKey);
|
|
340
|
+
}, [hasUnsaved, pendingKey]);
|
|
341
|
+
|
|
342
|
+
const resetToInitial = useCallback(() => {
|
|
343
|
+
const base = initialSnapshotRef.current || {};
|
|
344
|
+
setSdk(base.sdk || "auto");
|
|
345
|
+
setModel(base.model || "");
|
|
346
|
+
setTaskIdInput(base.taskIdInput || "");
|
|
347
|
+
showToast("Changes discarded", "info");
|
|
348
|
+
}, []);
|
|
301
349
|
|
|
302
|
-
const handleStart = async () => {
|
|
350
|
+
const handleStart = async ({ closeAfterStart = true } = {}) => {
|
|
303
351
|
if (starting) return;
|
|
304
352
|
if (!resolvedTaskId) {
|
|
305
353
|
showToast("Task ID is required", "error");
|
|
306
|
-
return;
|
|
354
|
+
return false;
|
|
307
355
|
}
|
|
308
356
|
setStarting(true);
|
|
309
357
|
try {
|
|
@@ -312,15 +360,37 @@ export function StartTaskModal({
|
|
|
312
360
|
sdk: sdk && sdk !== "auto" ? sdk : undefined,
|
|
313
361
|
model: model.trim() ? model.trim() : undefined,
|
|
314
362
|
});
|
|
315
|
-
|
|
363
|
+
initialSnapshotRef.current = {
|
|
364
|
+
sdk: sdk || "auto",
|
|
365
|
+
model: model || "",
|
|
366
|
+
taskIdInput: taskIdInput || "",
|
|
367
|
+
};
|
|
368
|
+
if (closeAfterStart) {
|
|
369
|
+
onClose?.();
|
|
370
|
+
return { closed: true };
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
316
373
|
} catch {
|
|
317
374
|
/* toast via apiFetch */
|
|
375
|
+
return false;
|
|
376
|
+
} finally {
|
|
377
|
+
setStarting(false);
|
|
318
378
|
}
|
|
319
|
-
setStarting(false);
|
|
320
379
|
};
|
|
321
380
|
|
|
322
381
|
return html`
|
|
323
|
-
<${Modal}
|
|
382
|
+
<${Modal}
|
|
383
|
+
title="Start Task"
|
|
384
|
+
onClose=${onClose}
|
|
385
|
+
contentClassName="modal-content-wide"
|
|
386
|
+
unsavedChanges=${changeCount}
|
|
387
|
+
onSaveBeforeClose=${() => handleStart({ closeAfterStart: true })}
|
|
388
|
+
onDiscardBeforeClose=${() => {
|
|
389
|
+
resetToInitial();
|
|
390
|
+
return true;
|
|
391
|
+
}}
|
|
392
|
+
activeOperationLabel=${starting ? "Task dispatch is still running" : ""}
|
|
393
|
+
>
|
|
324
394
|
${task?.id || task?.title
|
|
325
395
|
? html`
|
|
326
396
|
<div class="meta-text mb-sm">
|
|
@@ -363,13 +433,26 @@ export function StartTaskModal({
|
|
|
363
433
|
<div class="modal-form-field modal-form-span">
|
|
364
434
|
<button
|
|
365
435
|
class="btn btn-primary"
|
|
366
|
-
onClick=${
|
|
436
|
+
onClick=${() => {
|
|
437
|
+
void handleStart({ closeAfterStart: true });
|
|
438
|
+
}}
|
|
367
439
|
disabled=${starting || !resolvedTaskId}
|
|
368
440
|
>
|
|
369
441
|
${starting ? "Starting…" : ":play: Start Task"}
|
|
370
442
|
</button>
|
|
371
443
|
</div>
|
|
372
444
|
</div>
|
|
445
|
+
<${SaveDiscardBar}
|
|
446
|
+
dirty=${hasUnsaved}
|
|
447
|
+
message=${unsavedChangesMessage(changeCount)}
|
|
448
|
+
saveLabel="Start Task"
|
|
449
|
+
discardLabel="Discard"
|
|
450
|
+
onSave=${() => {
|
|
451
|
+
void handleStart({ closeAfterStart: false });
|
|
452
|
+
}}
|
|
453
|
+
onDiscard=${resetToInitial}
|
|
454
|
+
saving=${starting}
|
|
455
|
+
/>
|
|
373
456
|
<//>
|
|
374
457
|
`;
|
|
375
458
|
}
|
|
@@ -538,6 +621,21 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
538
621
|
const [defaults, setDefaults] = useState({ executor: "auto", model: "auto" });
|
|
539
622
|
const [templates, setTemplates] = useState([]);
|
|
540
623
|
const [planner, setPlanner] = useState({});
|
|
624
|
+
const defaultsBaselineRef = useRef({ executor: "auto", model: "auto" });
|
|
625
|
+
const pendingKey = "modal:trigger-templates";
|
|
626
|
+
|
|
627
|
+
const defaultsSnapshot = useMemo(
|
|
628
|
+
() => ({
|
|
629
|
+
executor: String(defaults?.executor || "auto"),
|
|
630
|
+
model: String(defaults?.model || "auto"),
|
|
631
|
+
}),
|
|
632
|
+
[defaults],
|
|
633
|
+
);
|
|
634
|
+
const defaultsDirtyCount = useMemo(
|
|
635
|
+
() => countChangedFields(defaultsBaselineRef.current, defaultsSnapshot),
|
|
636
|
+
[defaultsSnapshot],
|
|
637
|
+
);
|
|
638
|
+
const hasUnsavedDefaults = defaultsDirtyCount > 0;
|
|
541
639
|
|
|
542
640
|
const loadTemplates = useCallback(async () => {
|
|
543
641
|
setLoading(true);
|
|
@@ -546,11 +644,15 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
546
644
|
const res = await apiFetch("/api/triggers/templates", { _silent: true });
|
|
547
645
|
const data = res?.data || {};
|
|
548
646
|
setEnabled(data.enabled === true);
|
|
549
|
-
|
|
647
|
+
const normalizedDefaults =
|
|
550
648
|
data.defaults && typeof data.defaults === "object"
|
|
551
649
|
? data.defaults
|
|
552
|
-
: { executor: "auto", model: "auto" }
|
|
553
|
-
|
|
650
|
+
: { executor: "auto", model: "auto" };
|
|
651
|
+
defaultsBaselineRef.current = {
|
|
652
|
+
executor: String(normalizedDefaults.executor || "auto"),
|
|
653
|
+
model: String(normalizedDefaults.model || "auto"),
|
|
654
|
+
};
|
|
655
|
+
setDefaults(normalizedDefaults);
|
|
554
656
|
setTemplates(Array.isArray(data.templates) ? data.templates : []);
|
|
555
657
|
setPlanner(data.planner || {});
|
|
556
658
|
} catch (err) {
|
|
@@ -563,6 +665,11 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
563
665
|
loadTemplates();
|
|
564
666
|
}, []);
|
|
565
667
|
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
setPendingChange(pendingKey, hasUnsavedDefaults);
|
|
670
|
+
return () => clearPendingChange(pendingKey);
|
|
671
|
+
}, [hasUnsavedDefaults]);
|
|
672
|
+
|
|
566
673
|
const persistUpdate = async (payload) => {
|
|
567
674
|
setSaving(true);
|
|
568
675
|
setError("");
|
|
@@ -573,11 +680,15 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
573
680
|
});
|
|
574
681
|
const data = res?.data || {};
|
|
575
682
|
setEnabled(data.enabled === true);
|
|
576
|
-
|
|
683
|
+
const normalizedDefaults =
|
|
577
684
|
data.defaults && typeof data.defaults === "object"
|
|
578
685
|
? data.defaults
|
|
579
|
-
: { executor: "auto", model: "auto" }
|
|
580
|
-
|
|
686
|
+
: { executor: "auto", model: "auto" };
|
|
687
|
+
defaultsBaselineRef.current = {
|
|
688
|
+
executor: String(normalizedDefaults.executor || "auto"),
|
|
689
|
+
model: String(normalizedDefaults.model || "auto"),
|
|
690
|
+
};
|
|
691
|
+
setDefaults(normalizedDefaults);
|
|
581
692
|
setTemplates(Array.isArray(data.templates) ? data.templates : []);
|
|
582
693
|
setPlanner(data.planner || {});
|
|
583
694
|
showToast("Template settings updated", "success");
|
|
@@ -599,10 +710,28 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
599
710
|
}
|
|
600
711
|
};
|
|
601
712
|
|
|
602
|
-
const handleSaveDefaults = async () => {
|
|
603
|
-
|
|
713
|
+
const handleSaveDefaults = async ({ closeAfterSave = false } = {}) => {
|
|
714
|
+
try {
|
|
715
|
+
await persistUpdate({ defaults });
|
|
716
|
+
if (closeAfterSave) {
|
|
717
|
+
onClose?.();
|
|
718
|
+
return { closed: true };
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
} catch {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
604
724
|
};
|
|
605
725
|
|
|
726
|
+
const handleDiscardDefaults = useCallback(() => {
|
|
727
|
+
const base = defaultsBaselineRef.current || { executor: "auto", model: "auto" };
|
|
728
|
+
setDefaults({
|
|
729
|
+
executor: base.executor || "auto",
|
|
730
|
+
model: base.model || "auto",
|
|
731
|
+
});
|
|
732
|
+
showToast("Changes discarded", "info");
|
|
733
|
+
}, []);
|
|
734
|
+
|
|
606
735
|
const handleToggleTemplate = async (template, nextEnabled) => {
|
|
607
736
|
await persistUpdate({ template: { ...template, enabled: nextEnabled } });
|
|
608
737
|
};
|
|
@@ -616,6 +745,13 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
616
745
|
title="Trigger Templates"
|
|
617
746
|
onClose=${onClose}
|
|
618
747
|
contentClassName="modal-content-wide"
|
|
748
|
+
unsavedChanges=${defaultsDirtyCount}
|
|
749
|
+
onSaveBeforeClose=${() => handleSaveDefaults({ closeAfterSave: true })}
|
|
750
|
+
onDiscardBeforeClose=${() => {
|
|
751
|
+
handleDiscardDefaults();
|
|
752
|
+
return true;
|
|
753
|
+
}}
|
|
754
|
+
activeOperationLabel=${saving ? "Template update request is still running" : ""}
|
|
619
755
|
>
|
|
620
756
|
<div class="flex-col" style="gap:10px;">
|
|
621
757
|
<div class="card" style="padding:10px 12px;">
|
|
@@ -689,6 +825,17 @@ function TriggerTemplatesModal({ onClose }) {
|
|
|
689
825
|
onSaveTemplate=${handleSaveTemplate}
|
|
690
826
|
/>
|
|
691
827
|
`)}
|
|
828
|
+
<${SaveDiscardBar}
|
|
829
|
+
dirty=${hasUnsavedDefaults}
|
|
830
|
+
message=${unsavedChangesMessage(defaultsDirtyCount)}
|
|
831
|
+
saveLabel="Save Defaults"
|
|
832
|
+
discardLabel="Discard"
|
|
833
|
+
onSave=${() => {
|
|
834
|
+
void handleSaveDefaults({ closeAfterSave: false });
|
|
835
|
+
}}
|
|
836
|
+
onDiscard=${handleDiscardDefaults}
|
|
837
|
+
saving=${saving}
|
|
838
|
+
/>
|
|
692
839
|
</div>
|
|
693
840
|
<//>
|
|
694
841
|
`;
|
|
@@ -1252,9 +1399,49 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1252
1399
|
);
|
|
1253
1400
|
const [repository, setRepository] = useState(task?.repository || "");
|
|
1254
1401
|
const attachmentInputRef = useRef(null);
|
|
1402
|
+
const initialSnapshotRef = useRef({
|
|
1403
|
+
title: task?.title || "",
|
|
1404
|
+
description: task?.description || "",
|
|
1405
|
+
baseBranch: getTaskBaseBranch(task),
|
|
1406
|
+
status: task?.status || "todo",
|
|
1407
|
+
priority: task?.priority || "",
|
|
1408
|
+
tagsInput: getTaskTags(task).join(", "),
|
|
1409
|
+
draft: Boolean(task?.draft || task?.status === "draft"),
|
|
1410
|
+
});
|
|
1411
|
+
const pendingKey = useMemo(
|
|
1412
|
+
() => `modal:task-detail:${task?.id || "unknown"}`,
|
|
1413
|
+
[task?.id],
|
|
1414
|
+
);
|
|
1255
1415
|
const activeWsId = activeWorkspaceId.value || "";
|
|
1256
1416
|
const canDispatch = Boolean(onStart && task?.id);
|
|
1257
1417
|
|
|
1418
|
+
const editableSnapshot = useMemo(
|
|
1419
|
+
() => ({
|
|
1420
|
+
title: title || "",
|
|
1421
|
+
description: description || "",
|
|
1422
|
+
baseBranch: baseBranch || "",
|
|
1423
|
+
status: status || "todo",
|
|
1424
|
+
priority: priority || "",
|
|
1425
|
+
tagsInput: tagsInput || "",
|
|
1426
|
+
draft: Boolean(draft),
|
|
1427
|
+
}),
|
|
1428
|
+
[baseBranch, description, draft, priority, status, tagsInput, title],
|
|
1429
|
+
);
|
|
1430
|
+
const changeCount = useMemo(
|
|
1431
|
+
() => countChangedFields(initialSnapshotRef.current, editableSnapshot),
|
|
1432
|
+
[editableSnapshot],
|
|
1433
|
+
);
|
|
1434
|
+
const hasUnsaved = changeCount > 0;
|
|
1435
|
+
const activeOperationLabel = saving
|
|
1436
|
+
? "Task save is in progress"
|
|
1437
|
+
: rewriting
|
|
1438
|
+
? "Improve with AI is still running"
|
|
1439
|
+
: uploadingAttachment
|
|
1440
|
+
? "Attachment upload is still running"
|
|
1441
|
+
: manualBusy
|
|
1442
|
+
? "Manual takeover update is in progress"
|
|
1443
|
+
: "";
|
|
1444
|
+
|
|
1258
1445
|
const workspaceOptions = managedWorkspaces.value || [];
|
|
1259
1446
|
const selectedWorkspace = useMemo(
|
|
1260
1447
|
() => workspaceOptions.find((ws) => ws.id === workspaceId) || null,
|
|
@@ -1263,19 +1450,35 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1263
1450
|
const repositoryOptions = selectedWorkspace?.repos || [];
|
|
1264
1451
|
|
|
1265
1452
|
useEffect(() => {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1453
|
+
const nextTitle = task?.title || "";
|
|
1454
|
+
const nextDescription = task?.description || "";
|
|
1455
|
+
const nextBaseBranch = getTaskBaseBranch(task);
|
|
1456
|
+
const nextStatus = task?.status || "todo";
|
|
1457
|
+
const nextPriority = task?.priority || "";
|
|
1458
|
+
const nextTags = getTaskTags(task).join(", ");
|
|
1459
|
+
const nextDraft = Boolean(task?.draft || task?.status === "draft");
|
|
1460
|
+
setTitle(nextTitle);
|
|
1461
|
+
setDescription(nextDescription);
|
|
1462
|
+
setBaseBranch(nextBaseBranch);
|
|
1463
|
+
setStatus(nextStatus);
|
|
1464
|
+
setPriority(nextPriority);
|
|
1465
|
+
setTagsInput(nextTags);
|
|
1272
1466
|
setAttachments(normalizeTaskAttachments(task));
|
|
1273
1467
|
setComments(normalizeTaskComments(task));
|
|
1274
|
-
setDraft(
|
|
1468
|
+
setDraft(nextDraft);
|
|
1275
1469
|
setManualOverride(isTaskManual(task));
|
|
1276
1470
|
setManualReason(getManualReason(task));
|
|
1277
1471
|
setWorkspaceId(task?.workspace || activeWorkspaceId.value || "");
|
|
1278
1472
|
setRepository(task?.repository || "");
|
|
1473
|
+
initialSnapshotRef.current = {
|
|
1474
|
+
title: nextTitle,
|
|
1475
|
+
description: nextDescription,
|
|
1476
|
+
baseBranch: nextBaseBranch,
|
|
1477
|
+
status: nextStatus,
|
|
1478
|
+
priority: nextPriority,
|
|
1479
|
+
tagsInput: nextTags,
|
|
1480
|
+
draft: nextDraft,
|
|
1481
|
+
};
|
|
1279
1482
|
}, [task?.id]);
|
|
1280
1483
|
|
|
1281
1484
|
useEffect(() => {
|
|
@@ -1300,7 +1503,24 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1300
1503
|
}
|
|
1301
1504
|
}, [workspaceId, repositoryOptions.length]);
|
|
1302
1505
|
|
|
1303
|
-
|
|
1506
|
+
useEffect(() => {
|
|
1507
|
+
setPendingChange(pendingKey, hasUnsaved);
|
|
1508
|
+
return () => clearPendingChange(pendingKey);
|
|
1509
|
+
}, [hasUnsaved, pendingKey]);
|
|
1510
|
+
|
|
1511
|
+
const handleDiscardChanges = useCallback(() => {
|
|
1512
|
+
const base = initialSnapshotRef.current || {};
|
|
1513
|
+
setTitle(base.title || "");
|
|
1514
|
+
setDescription(base.description || "");
|
|
1515
|
+
setBaseBranch(base.baseBranch || "");
|
|
1516
|
+
setStatus(base.status || "todo");
|
|
1517
|
+
setPriority(base.priority || "");
|
|
1518
|
+
setTagsInput(base.tagsInput || "");
|
|
1519
|
+
setDraft(Boolean(base.draft));
|
|
1520
|
+
showToast("Changes discarded", "info");
|
|
1521
|
+
}, []);
|
|
1522
|
+
|
|
1523
|
+
const handleSave = async ({ closeAfterSave = true } = {}) => {
|
|
1304
1524
|
setSaving(true);
|
|
1305
1525
|
haptic("medium");
|
|
1306
1526
|
const prev = cloneValue(tasksData.value);
|
|
@@ -1354,11 +1574,26 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1354
1574
|
},
|
|
1355
1575
|
);
|
|
1356
1576
|
showToast("Task saved", "success");
|
|
1357
|
-
|
|
1577
|
+
initialSnapshotRef.current = {
|
|
1578
|
+
title,
|
|
1579
|
+
description,
|
|
1580
|
+
baseBranch,
|
|
1581
|
+
status: nextStatus,
|
|
1582
|
+
priority: priority || "",
|
|
1583
|
+
tagsInput,
|
|
1584
|
+
draft: wantsDraft,
|
|
1585
|
+
};
|
|
1586
|
+
if (closeAfterSave) {
|
|
1587
|
+
onClose?.();
|
|
1588
|
+
return { closed: true };
|
|
1589
|
+
}
|
|
1590
|
+
return true;
|
|
1358
1591
|
} catch {
|
|
1359
1592
|
/* toast via apiFetch */
|
|
1593
|
+
return false;
|
|
1594
|
+
} finally {
|
|
1595
|
+
setSaving(false);
|
|
1360
1596
|
}
|
|
1361
|
-
setSaving(false);
|
|
1362
1597
|
};
|
|
1363
1598
|
|
|
1364
1599
|
const handleStatusUpdate = async (newStatus) => {
|
|
@@ -1527,7 +1762,18 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1527
1762
|
};
|
|
1528
1763
|
|
|
1529
1764
|
return html`
|
|
1530
|
-
<${Modal}
|
|
1765
|
+
<${Modal}
|
|
1766
|
+
title=${task?.title || "Task Detail"}
|
|
1767
|
+
onClose=${onClose}
|
|
1768
|
+
contentClassName="modal-content-wide"
|
|
1769
|
+
unsavedChanges=${changeCount}
|
|
1770
|
+
onSaveBeforeClose=${() => handleSave({ closeAfterSave: true })}
|
|
1771
|
+
onDiscardBeforeClose=${() => {
|
|
1772
|
+
handleDiscardChanges();
|
|
1773
|
+
return true;
|
|
1774
|
+
}}
|
|
1775
|
+
activeOperationLabel=${activeOperationLabel}
|
|
1776
|
+
>
|
|
1531
1777
|
<div class="task-modal-summary">
|
|
1532
1778
|
<div class="task-modal-id" style="user-select:all">ID: ${task?.id}</div>
|
|
1533
1779
|
<div class="task-modal-badges">
|
|
@@ -1824,6 +2070,19 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1824
2070
|
</div>
|
|
1825
2071
|
`}
|
|
1826
2072
|
|
|
2073
|
+
<${SaveDiscardBar}
|
|
2074
|
+
dirty=${hasUnsaved}
|
|
2075
|
+
message=${unsavedChangesMessage(changeCount)}
|
|
2076
|
+
saveLabel="Save Changes"
|
|
2077
|
+
discardLabel="Discard"
|
|
2078
|
+
onSave=${() => {
|
|
2079
|
+
void handleSave({ closeAfterSave: false });
|
|
2080
|
+
}}
|
|
2081
|
+
onDiscard=${handleDiscardChanges}
|
|
2082
|
+
saving=${saving}
|
|
2083
|
+
disabled=${Boolean(activeOperationLabel && !saving)}
|
|
2084
|
+
/>
|
|
2085
|
+
|
|
1827
2086
|
<div class="btn-row modal-form-span">
|
|
1828
2087
|
${(task?.status === "error" || task?.status === "cancelled") &&
|
|
1829
2088
|
html`
|
|
@@ -1833,7 +2092,9 @@ export function TaskDetailModal({ task, onClose, onStart }) {
|
|
|
1833
2092
|
`}
|
|
1834
2093
|
<button
|
|
1835
2094
|
class="btn btn-secondary btn-sm"
|
|
1836
|
-
onClick=${
|
|
2095
|
+
onClick=${() => {
|
|
2096
|
+
void handleSave({ closeAfterSave: true });
|
|
2097
|
+
}}
|
|
1837
2098
|
disabled=${saving}
|
|
1838
2099
|
>
|
|
1839
2100
|
${saving ? "Saving…" : iconText(":save: Save")}
|
|
@@ -3008,6 +3269,15 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3008
3269
|
const [repository, setRepository] = useState("");
|
|
3009
3270
|
const [repositories, setRepositories] = useState([]);
|
|
3010
3271
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
3272
|
+
const initialSnapshotRef = useRef({
|
|
3273
|
+
title: "",
|
|
3274
|
+
description: "",
|
|
3275
|
+
baseBranch: "",
|
|
3276
|
+
priority: "medium",
|
|
3277
|
+
tagsInput: "",
|
|
3278
|
+
draft: false,
|
|
3279
|
+
});
|
|
3280
|
+
const pendingKey = "modal:create-task-inline";
|
|
3011
3281
|
|
|
3012
3282
|
const handleRewrite = async () => {
|
|
3013
3283
|
if (!title.trim() || rewriting) return;
|
|
@@ -3063,6 +3333,39 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3063
3333
|
}
|
|
3064
3334
|
}, [workspaceId, repositoryOptions.length]);
|
|
3065
3335
|
|
|
3336
|
+
const unsavedSnapshot = useMemo(
|
|
3337
|
+
() => ({
|
|
3338
|
+
title: title || "",
|
|
3339
|
+
description: description || "",
|
|
3340
|
+
baseBranch: baseBranch || "",
|
|
3341
|
+
priority: priority || "medium",
|
|
3342
|
+
tagsInput: tagsInput || "",
|
|
3343
|
+
draft: Boolean(draft),
|
|
3344
|
+
}),
|
|
3345
|
+
[baseBranch, description, draft, priority, tagsInput, title],
|
|
3346
|
+
);
|
|
3347
|
+
const changeCount = useMemo(
|
|
3348
|
+
() => countChangedFields(initialSnapshotRef.current, unsavedSnapshot),
|
|
3349
|
+
[unsavedSnapshot],
|
|
3350
|
+
);
|
|
3351
|
+
const hasUnsaved = changeCount > 0;
|
|
3352
|
+
|
|
3353
|
+
useEffect(() => {
|
|
3354
|
+
setPendingChange(pendingKey, hasUnsaved);
|
|
3355
|
+
return () => clearPendingChange(pendingKey);
|
|
3356
|
+
}, [hasUnsaved]);
|
|
3357
|
+
|
|
3358
|
+
const resetToInitial = useCallback(() => {
|
|
3359
|
+
const base = initialSnapshotRef.current || {};
|
|
3360
|
+
setTitle(base.title || "");
|
|
3361
|
+
setDescription(base.description || "");
|
|
3362
|
+
setBaseBranch(base.baseBranch || "");
|
|
3363
|
+
setPriority(base.priority || "medium");
|
|
3364
|
+
setTagsInput(base.tagsInput || "");
|
|
3365
|
+
setDraft(Boolean(base.draft));
|
|
3366
|
+
showToast("Changes discarded", "info");
|
|
3367
|
+
}, []);
|
|
3368
|
+
|
|
3066
3369
|
const toggleRepo = (slug) => {
|
|
3067
3370
|
setRepositories((prev) =>
|
|
3068
3371
|
prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug],
|
|
@@ -3077,10 +3380,14 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3077
3380
|
});
|
|
3078
3381
|
};
|
|
3079
3382
|
|
|
3080
|
-
const handleSubmit = async () => {
|
|
3383
|
+
const handleSubmit = async ({ closeAfterSave = true } = {}) => {
|
|
3384
|
+
if (rewriting) {
|
|
3385
|
+
showToast("Wait for AI improvement to finish before saving.", "warning");
|
|
3386
|
+
return false;
|
|
3387
|
+
}
|
|
3081
3388
|
if (!title.trim()) {
|
|
3082
3389
|
showToast("Title is required", "error");
|
|
3083
|
-
return;
|
|
3390
|
+
return false;
|
|
3084
3391
|
}
|
|
3085
3392
|
setSubmitting(true);
|
|
3086
3393
|
haptic("medium");
|
|
@@ -3103,12 +3410,25 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3103
3410
|
}),
|
|
3104
3411
|
});
|
|
3105
3412
|
showToast("Task created", "success");
|
|
3106
|
-
|
|
3413
|
+
initialSnapshotRef.current = {
|
|
3414
|
+
title: title.trim(),
|
|
3415
|
+
description: description.trim(),
|
|
3416
|
+
baseBranch: baseBranch.trim(),
|
|
3417
|
+
priority,
|
|
3418
|
+
tagsInput,
|
|
3419
|
+
draft: Boolean(draft),
|
|
3420
|
+
};
|
|
3421
|
+
if (closeAfterSave) {
|
|
3422
|
+
onClose?.();
|
|
3423
|
+
}
|
|
3107
3424
|
await loadTasks();
|
|
3425
|
+
return closeAfterSave ? { closed: true } : true;
|
|
3108
3426
|
} catch {
|
|
3109
3427
|
/* toast */
|
|
3428
|
+
return false;
|
|
3429
|
+
} finally {
|
|
3430
|
+
setSubmitting(false);
|
|
3110
3431
|
}
|
|
3111
|
-
setSubmitting(false);
|
|
3112
3432
|
};
|
|
3113
3433
|
|
|
3114
3434
|
useEffect(() => {
|
|
@@ -3129,8 +3449,10 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3129
3449
|
priority,
|
|
3130
3450
|
tagsInput,
|
|
3131
3451
|
draft,
|
|
3452
|
+
rewriting,
|
|
3132
3453
|
workspaceId,
|
|
3133
3454
|
repository,
|
|
3455
|
+
repositories,
|
|
3134
3456
|
]);
|
|
3135
3457
|
|
|
3136
3458
|
const parsedTags = normalizeTagInput(tagsInput);
|
|
@@ -3140,8 +3462,10 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3140
3462
|
<button
|
|
3141
3463
|
class="btn btn-primary"
|
|
3142
3464
|
style="width:100%"
|
|
3143
|
-
onClick=${
|
|
3144
|
-
|
|
3465
|
+
onClick=${() => {
|
|
3466
|
+
void handleSubmit({ closeAfterSave: true });
|
|
3467
|
+
}}
|
|
3468
|
+
disabled=${submitting || rewriting}
|
|
3145
3469
|
>
|
|
3146
3470
|
${submitting ? "Creating…" : iconText("✓ Create Task")}
|
|
3147
3471
|
</button>
|
|
@@ -3153,6 +3477,13 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3153
3477
|
onClose=${onClose}
|
|
3154
3478
|
contentClassName="modal-content-wide"
|
|
3155
3479
|
footer=${footerContent}
|
|
3480
|
+
unsavedChanges=${changeCount}
|
|
3481
|
+
onSaveBeforeClose=${() => handleSubmit({ closeAfterSave: true })}
|
|
3482
|
+
onDiscardBeforeClose=${() => {
|
|
3483
|
+
resetToInitial();
|
|
3484
|
+
return true;
|
|
3485
|
+
}}
|
|
3486
|
+
activeOperationLabel=${rewriting ? "Improve with AI is still running" : ""}
|
|
3156
3487
|
>
|
|
3157
3488
|
<div class="flex-col create-task-form">
|
|
3158
3489
|
|
|
@@ -3164,7 +3495,12 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3164
3495
|
value=${title}
|
|
3165
3496
|
autoFocus=${true}
|
|
3166
3497
|
onInput=${(e) => setTitle(e.target.value)}
|
|
3167
|
-
onKeyDown=${(e) =>
|
|
3498
|
+
onKeyDown=${(e) => {
|
|
3499
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
3500
|
+
e.preventDefault();
|
|
3501
|
+
void handleSubmit({ closeAfterSave: true });
|
|
3502
|
+
}
|
|
3503
|
+
}}
|
|
3168
3504
|
/>
|
|
3169
3505
|
<${VoiceMicButtonInline}
|
|
3170
3506
|
onTranscript=${(t) => setTitle((prev) => (prev ? prev + " " + t : t))}
|
|
@@ -3300,6 +3636,19 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
3300
3636
|
/>
|
|
3301
3637
|
`}
|
|
3302
3638
|
|
|
3639
|
+
<${SaveDiscardBar}
|
|
3640
|
+
dirty=${hasUnsaved}
|
|
3641
|
+
message=${unsavedChangesMessage(changeCount)}
|
|
3642
|
+
saveLabel="Create Task"
|
|
3643
|
+
discardLabel="Discard"
|
|
3644
|
+
onSave=${() => {
|
|
3645
|
+
void handleSubmit({ closeAfterSave: false });
|
|
3646
|
+
}}
|
|
3647
|
+
onDiscard=${resetToInitial}
|
|
3648
|
+
saving=${submitting}
|
|
3649
|
+
disabled=${rewriting}
|
|
3650
|
+
/>
|
|
3651
|
+
|
|
3303
3652
|
</div>
|
|
3304
3653
|
<//>
|
|
3305
3654
|
`;
|
package/ui/tabs/telemetry.js
CHANGED
package/ui/tabs/workflows.js
CHANGED
|
@@ -2339,6 +2339,10 @@ export function WorkflowsTab() {
|
|
|
2339
2339
|
return html`
|
|
2340
2340
|
<style>
|
|
2341
2341
|
.wf-btn {
|
|
2342
|
+
display: inline-flex;
|
|
2343
|
+
align-items: center;
|
|
2344
|
+
justify-content: center;
|
|
2345
|
+
gap: 6px;
|
|
2342
2346
|
padding: 6px 14px;
|
|
2343
2347
|
border: 1px solid var(--color-border, #2a3040);
|
|
2344
2348
|
border-radius: 8px;
|