@linxin666/dsh-client-ui-task-board 0.1.19 → 0.2.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.i18n.yaml +2 -2
- package/README.md +82 -104
- package/README.zh.md +82 -104
- package/lib/client.js +589 -602
- package/lib/client.js.map +1 -1
- package/lib/index.js +2187 -6
- package/lib/types/client/PluginSettingsCard.d.ts +6 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +7 -4
- package/lib/types/client/board/TaskCard.d.ts +5 -2
- package/lib/types/client/board/status-key.d.ts +4 -0
- package/lib/types/client/host-api.d.ts +18 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/core/controller.d.ts +40 -15
- package/lib/types/core/schedule.d.ts +4 -2
- package/lib/types/core/tasks.d.ts +7 -5
- package/lib/types/core/use-cases/task-archive.d.ts +5 -4
- package/lib/types/core/use-cases/task-schedule.d.ts +3 -3
- package/lib/types/dsh-home.d.ts +16 -0
- package/lib/types/host-ledger.d.ts +42 -0
- package/lib/types/host-routes.d.ts +24 -0
- package/lib/types/host-runner.d.ts +30 -0
- package/lib/types/host-service.d.ts +41 -0
- package/lib/types/index.d.ts +20 -10
- package/lib/types/loopback.d.ts +24 -0
- package/lib/types/mount-once.d.ts +24 -0
- package/lib/types/power-inhibitor.d.ts +73 -0
- package/lib/types/protocol.d.ts +72 -0
- package/package.json +12 -9
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/TaskBoardSettingsCard.tsx +51 -4
- package/src/client/board/NewTaskModal.tsx +9 -6
- package/src/client/board/TaskBoard.tsx +22 -13
- package/src/client/board/TaskCard.tsx +24 -8
- package/src/client/board/TaskDetail.tsx +84 -59
- package/src/client/board/status-key.ts +11 -0
- package/src/client/host-api.ts +95 -0
- package/src/client/index.ts +9 -78
- package/src/client/locales.ts +24 -4
- package/src/core/controller.ts +205 -24
- package/src/core/schedule.ts +20 -3
- package/src/core/store.ts +5 -14
- package/src/core/tasks.ts +7 -7
- package/src/core/use-cases/task-archive.ts +14 -5
- package/src/core/use-cases/task-schedule.ts +6 -5
- package/src/core/use-cases/task-update.ts +1 -6
- package/src/dsh-home.ts +36 -0
- package/src/host-ledger.ts +478 -0
- package/src/host-routes.ts +188 -0
- package/src/host-runner.ts +134 -0
- package/src/host-service.ts +209 -0
- package/src/index.ts +58 -12
- package/src/loopback.ts +63 -0
- package/src/mount-once.ts +48 -0
- package/src/power-inhibitor.ts +321 -0
- package/src/protocol.ts +213 -0
- package/lib/types/core/scheduler.d.ts +0 -67
- package/src/core/scheduler.ts +0 -151
package/lib/client.js
CHANGED
|
@@ -59,7 +59,7 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
return typeof value === "string" && ALL_STATUSES.includes(value);
|
|
60
60
|
}
|
|
61
61
|
/** Normalize one optional execution-target string: trim; blank collapses to undefined. */
|
|
62
|
-
function normalizeTargetId
|
|
62
|
+
function normalizeTargetId(value) {
|
|
63
63
|
const trimmed = value?.trim();
|
|
64
64
|
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
65
65
|
}
|
|
@@ -74,8 +74,8 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
createdAt: now,
|
|
75
75
|
updatedAt: now,
|
|
76
76
|
executions: [],
|
|
77
|
-
workspaceId: normalizeTargetId
|
|
78
|
-
mode: normalizeTargetId
|
|
77
|
+
workspaceId: normalizeTargetId(input.workspaceId),
|
|
78
|
+
mode: normalizeTargetId(input.mode),
|
|
79
79
|
permission: isTaskPermission(input.permission) ? input.permission : void 0
|
|
80
80
|
};
|
|
81
81
|
}
|
|
@@ -172,7 +172,8 @@ window.__ModuleLoader__.load({
|
|
|
172
172
|
/**
|
|
173
173
|
* Archive one task: only settled statuses (done/failed) can be archived;
|
|
174
174
|
* a running or not-yet-settled task stays on the board (its runner still
|
|
175
|
-
* owns its lifecycle).
|
|
175
|
+
* owns its lifecycle). Archiving disarms a schedule; already-archived tasks
|
|
176
|
+
* are a no-op.
|
|
176
177
|
*/
|
|
177
178
|
function applyArchiveTask(tasks, id, now) {
|
|
178
179
|
let applied = false;
|
|
@@ -181,8 +182,14 @@ window.__ModuleLoader__.load({
|
|
|
181
182
|
if (task.id !== id || task.archivedAt !== void 0) return task;
|
|
182
183
|
if (!ARCHIVABLE_STATUSES.includes(task.status)) return task;
|
|
183
184
|
applied = true;
|
|
185
|
+
const schedule = task.schedule === void 0 ? void 0 : {
|
|
186
|
+
...task.schedule,
|
|
187
|
+
enabled: false,
|
|
188
|
+
nextRunAt: void 0
|
|
189
|
+
};
|
|
184
190
|
return {
|
|
185
191
|
...task,
|
|
192
|
+
...schedule === void 0 ? {} : { schedule },
|
|
186
193
|
archivedAt: now,
|
|
187
194
|
updatedAt: now
|
|
188
195
|
};
|
|
@@ -249,20 +256,46 @@ window.__ModuleLoader__.load({
|
|
|
249
256
|
/**
|
|
250
257
|
* Compute the next matching instant after `fromMs` (ms epoch), in local time,
|
|
251
258
|
* at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
|
|
252
|
-
* of the matching minute's start, or undefined when
|
|
253
|
-
*
|
|
259
|
+
* of the matching minute's start, or undefined when the calendar constraint
|
|
260
|
+
* can never match (for example `0 0 30 2 *`). The five-year horizon includes
|
|
261
|
+
* a full leap cycle, so a valid February 29 schedule remains reachable from
|
|
262
|
+
* every non-leap year.
|
|
254
263
|
*/
|
|
255
264
|
function nextRunAtMs(expr, fromMs) {
|
|
256
265
|
const schedule = parseCron(expr);
|
|
257
266
|
if (schedule === null) return void 0;
|
|
267
|
+
if (!hasPossibleCalendarDay(schedule)) return void 0;
|
|
258
268
|
const from = new Date(fromMs);
|
|
259
269
|
const scan = new Date(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes() + 1, 0, 0);
|
|
260
|
-
const limitMs = fromMs + 366 * 24 * 60 * 60 * 1e3;
|
|
270
|
+
const limitMs = fromMs + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
261
271
|
while (scan.getTime() <= limitMs) {
|
|
262
272
|
if (matches(schedule, scan)) return scan.getTime();
|
|
263
273
|
scan.setMinutes(scan.getMinutes() + 1);
|
|
264
274
|
}
|
|
265
275
|
}
|
|
276
|
+
/** Reject impossible month/day pairs without spending the multi-year scan. */
|
|
277
|
+
function hasPossibleCalendarDay(schedule) {
|
|
278
|
+
if (schedule.dayWildcard || !schedule.weekdayWildcard) return true;
|
|
279
|
+
const maximumDay = /* @__PURE__ */ new Map([
|
|
280
|
+
[1, 31],
|
|
281
|
+
[2, 29],
|
|
282
|
+
[3, 31],
|
|
283
|
+
[4, 30],
|
|
284
|
+
[5, 31],
|
|
285
|
+
[6, 30],
|
|
286
|
+
[7, 31],
|
|
287
|
+
[8, 31],
|
|
288
|
+
[9, 30],
|
|
289
|
+
[10, 31],
|
|
290
|
+
[11, 30],
|
|
291
|
+
[12, 31]
|
|
292
|
+
]);
|
|
293
|
+
for (const month of schedule.months) {
|
|
294
|
+
const maximum = maximumDay.get(month) ?? 0;
|
|
295
|
+
if ([...schedule.days].some((day) => day <= maximum)) return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
266
299
|
/** Parse one comma-list field into the match set. */
|
|
267
300
|
function parseField(field, min, max, out) {
|
|
268
301
|
if (field === "*") {
|
|
@@ -366,9 +399,9 @@ window.__ModuleLoader__.load({
|
|
|
366
399
|
* the core cron parser (schedule.ts) and the withSchedule transition.
|
|
367
400
|
*/
|
|
368
401
|
/**
|
|
369
|
-
* Set
|
|
370
|
-
* untouched); an enabled rule computes the
|
|
371
|
-
*
|
|
402
|
+
* Set an on-board task's schedule rule. A blank or invalid cron, or an
|
|
403
|
+
* archived task, is rejected (state untouched); an enabled rule computes the
|
|
404
|
+
* next run instant immediately, a disabled one carries no next-run instant.
|
|
372
405
|
* @param tasks - current ledger.
|
|
373
406
|
* @param id - the task to schedule.
|
|
374
407
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
@@ -376,7 +409,7 @@ window.__ModuleLoader__.load({
|
|
|
376
409
|
*/
|
|
377
410
|
function applySetSchedule(tasks, id, patch, now) {
|
|
378
411
|
const task = tasks.find((candidate) => candidate.id === id);
|
|
379
|
-
if (task === void 0) return {
|
|
412
|
+
if (task === void 0 || task.archivedAt !== void 0) return {
|
|
380
413
|
tasks,
|
|
381
414
|
applied: false
|
|
382
415
|
};
|
|
@@ -388,6 +421,10 @@ window.__ModuleLoader__.load({
|
|
|
388
421
|
};
|
|
389
422
|
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
390
423
|
const nextRunAt = enabled ? nextRunAtMs(cron, now) : void 0;
|
|
424
|
+
if (enabled && nextRunAt === void 0) return {
|
|
425
|
+
tasks,
|
|
426
|
+
applied: false
|
|
427
|
+
};
|
|
391
428
|
return {
|
|
392
429
|
tasks: tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
|
|
393
430
|
enabled,
|
|
@@ -408,7 +445,7 @@ window.__ModuleLoader__.load({
|
|
|
408
445
|
* @param now - clock instant (ms epoch).
|
|
409
446
|
*/
|
|
410
447
|
function applyScheduleNextRun(tasks, id, nextRunAt, lastTriggeredAt, now) {
|
|
411
|
-
return tasks.map((task) => task.id === id && task.schedule !== void 0 ? withSchedule(task, {
|
|
448
|
+
return tasks.map((task) => task.id === id && task.archivedAt === void 0 && task.schedule !== void 0 ? withSchedule(task, {
|
|
412
449
|
nextRunAt,
|
|
413
450
|
lastTriggeredAt
|
|
414
451
|
}, now) : task);
|
|
@@ -425,10 +462,6 @@ window.__ModuleLoader__.load({
|
|
|
425
462
|
* back to the runtime default); an unknown permission string is ignored so
|
|
426
463
|
* stale UI can never persist a value the execution service rejects.
|
|
427
464
|
*/
|
|
428
|
-
/** Collapse a blank target string to undefined (clears the pin). */
|
|
429
|
-
function normalizeTargetId$1(value) {
|
|
430
|
-
return value !== void 0 && value.trim() === "" ? void 0 : value;
|
|
431
|
-
}
|
|
432
465
|
/** Keep an unknown permission string from entering the ledger. */
|
|
433
466
|
function normalizePermission(current, value) {
|
|
434
467
|
if (value === void 0) return void 0;
|
|
@@ -445,8 +478,8 @@ window.__ModuleLoader__.load({
|
|
|
445
478
|
function applyUpdateTask(tasks, id, patch, now) {
|
|
446
479
|
return tasks.map((task) => {
|
|
447
480
|
if (task.id !== id) return task;
|
|
448
|
-
const workspaceId = "workspaceId" in patch ? normalizeTargetId
|
|
449
|
-
const mode = "mode" in patch ? normalizeTargetId
|
|
481
|
+
const workspaceId = "workspaceId" in patch ? normalizeTargetId(patch.workspaceId) : void 0;
|
|
482
|
+
const mode = "mode" in patch ? normalizeTargetId(patch.mode) : void 0;
|
|
450
483
|
const permission = "permission" in patch ? normalizePermission(task.permission, patch.permission) : void 0;
|
|
451
484
|
const next = {
|
|
452
485
|
...task,
|
|
@@ -480,6 +513,9 @@ window.__ModuleLoader__.load({
|
|
|
480
513
|
function currentOf(sessions) {
|
|
481
514
|
return sessions.list.getSnapshot().current;
|
|
482
515
|
}
|
|
516
|
+
function messageOf(error) {
|
|
517
|
+
return error instanceof Error ? error.message : String(error);
|
|
518
|
+
}
|
|
483
519
|
/**
|
|
484
520
|
* Board controller (see module doc). All mutations bump the snapshot and
|
|
485
521
|
* persist through the store; UI and DOM mounts subscribe and re-render.
|
|
@@ -498,6 +534,12 @@ window.__ModuleLoader__.load({
|
|
|
498
534
|
disposers = [];
|
|
499
535
|
now;
|
|
500
536
|
uuid;
|
|
537
|
+
pendingTaskIds = /* @__PURE__ */ new Set();
|
|
538
|
+
taskQueues = /* @__PURE__ */ new Map();
|
|
539
|
+
transportError;
|
|
540
|
+
hostState;
|
|
541
|
+
remoteSubscribed = false;
|
|
542
|
+
remoteInitialization;
|
|
501
543
|
/** @param deps - store, execution service, and the sessions navigation face. */
|
|
502
544
|
constructor(deps) {
|
|
503
545
|
this.deps = deps;
|
|
@@ -507,11 +549,12 @@ window.__ModuleLoader__.load({
|
|
|
507
549
|
/** Load the persisted ledger and start the navigation/status subscriptions. */
|
|
508
550
|
start() {
|
|
509
551
|
this.tasks = this.deps.store.load();
|
|
510
|
-
this.
|
|
511
|
-
|
|
552
|
+
if (this.deps.transport !== void 0) this.initializeRemote();
|
|
553
|
+
else this.reconcileRunningTasks();
|
|
554
|
+
const unsubscribeExternal = this.deps.transport === void 0 ? this.deps.store.subscribeExternal?.(() => {
|
|
512
555
|
this.tasks = this.deps.store.load();
|
|
513
556
|
this.notify();
|
|
514
|
-
});
|
|
557
|
+
}) : void 0;
|
|
515
558
|
if (unsubscribeExternal !== void 0) this.disposers.push(unsubscribeExternal);
|
|
516
559
|
this.disposers.push(this.deps.sessions.list.subscribe(() => {
|
|
517
560
|
this.onSessionsChanged();
|
|
@@ -531,7 +574,10 @@ window.__ModuleLoader__.load({
|
|
|
531
574
|
boardOpen: this.boardOpen,
|
|
532
575
|
archiveView: this.archiveView,
|
|
533
576
|
selectedTaskId: this.selectedTaskId,
|
|
534
|
-
executionOptions: this.executionOptions
|
|
577
|
+
executionOptions: this.executionOptions,
|
|
578
|
+
pendingTaskIds: [...this.pendingTaskIds],
|
|
579
|
+
...this.transportError === void 0 ? {} : { transportError: this.transportError },
|
|
580
|
+
...this.hostState === void 0 ? {} : { host: this.hostState }
|
|
535
581
|
};
|
|
536
582
|
}
|
|
537
583
|
subscribe(fn) {
|
|
@@ -540,6 +586,14 @@ window.__ModuleLoader__.load({
|
|
|
540
586
|
this.listeners.delete(fn);
|
|
541
587
|
};
|
|
542
588
|
}
|
|
589
|
+
/** Whether production mutations are confirmed by the Host transport. */
|
|
590
|
+
isHostBacked() {
|
|
591
|
+
return this.deps.transport !== void 0;
|
|
592
|
+
}
|
|
593
|
+
/** Retry initial migration/state synchronization after an explicit Host error. */
|
|
594
|
+
async retryHostSync() {
|
|
595
|
+
return await this.initializeRemote();
|
|
596
|
+
}
|
|
543
597
|
openBoard() {
|
|
544
598
|
if (this.boardOpen) return;
|
|
545
599
|
this.lastCurrent = currentOf(this.deps.sessions);
|
|
@@ -579,13 +633,33 @@ window.__ModuleLoader__.load({
|
|
|
579
633
|
this.notify();
|
|
580
634
|
}
|
|
581
635
|
createTask(input) {
|
|
582
|
-
const
|
|
636
|
+
const id = this.uuid();
|
|
637
|
+
const { task, tasks } = applyCreateTask(this.tasks, input, this.now(), id);
|
|
583
638
|
if (task === void 0) return void 0;
|
|
584
639
|
this.tasks = [...tasks];
|
|
585
640
|
this.persistAndNotify();
|
|
586
641
|
return task;
|
|
587
642
|
}
|
|
643
|
+
/** Create through the Host and expose the task only after confirmation. */
|
|
644
|
+
async createTaskConfirmed(input) {
|
|
645
|
+
if (this.deps.transport === void 0) return this.createTask(input);
|
|
646
|
+
const id = this.uuid();
|
|
647
|
+
if (applyCreateTask(this.tasks, input, this.now(), id).task === void 0) return void 0;
|
|
648
|
+
return await this.commitRemote({
|
|
649
|
+
kind: "create",
|
|
650
|
+
id,
|
|
651
|
+
input
|
|
652
|
+
}, id) ? this.tasks.find((task) => task.id === id) : void 0;
|
|
653
|
+
}
|
|
588
654
|
updateTask(id, patch) {
|
|
655
|
+
if (this.deps.transport !== void 0) {
|
|
656
|
+
this.commitRemote({
|
|
657
|
+
kind: "update",
|
|
658
|
+
taskId: id,
|
|
659
|
+
patch
|
|
660
|
+
}, id);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
589
663
|
this.tasks = [...applyUpdateTask(this.tasks, id, patch, this.now())];
|
|
590
664
|
this.persistAndNotify();
|
|
591
665
|
}
|
|
@@ -601,10 +675,25 @@ window.__ModuleLoader__.load({
|
|
|
601
675
|
this.notify();
|
|
602
676
|
}
|
|
603
677
|
moveTask(id, status) {
|
|
678
|
+
if (this.deps.transport !== void 0) {
|
|
679
|
+
this.commitRemote({
|
|
680
|
+
kind: "move",
|
|
681
|
+
taskId: id,
|
|
682
|
+
status
|
|
683
|
+
}, id);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
604
686
|
this.tasks = this.tasks.map((task) => task.id === id ? withStatus(task, status, this.now()) : task);
|
|
605
687
|
this.persistAndNotify();
|
|
606
688
|
}
|
|
607
689
|
deleteTask(id) {
|
|
690
|
+
if (this.deps.transport !== void 0) {
|
|
691
|
+
this.commitRemote({
|
|
692
|
+
kind: "delete",
|
|
693
|
+
taskId: id
|
|
694
|
+
}, id);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
608
697
|
const { tasks, selectionCleared } = applyDeleteTask(this.tasks, this.selectedTaskId, id);
|
|
609
698
|
this.tasks = [...tasks];
|
|
610
699
|
if (selectionCleared) this.selectedTaskId = void 0;
|
|
@@ -619,6 +708,13 @@ window.__ModuleLoader__.load({
|
|
|
619
708
|
archiveTask(id) {
|
|
620
709
|
const { tasks, archived } = applyArchiveTask(this.tasks, id, this.now());
|
|
621
710
|
if (!archived) return false;
|
|
711
|
+
if (this.deps.transport !== void 0) {
|
|
712
|
+
this.commitRemote({
|
|
713
|
+
kind: "archive",
|
|
714
|
+
taskId: id
|
|
715
|
+
}, id);
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
622
718
|
this.tasks = [...tasks];
|
|
623
719
|
this.persistAndNotify();
|
|
624
720
|
return true;
|
|
@@ -627,7 +723,17 @@ window.__ModuleLoader__.load({
|
|
|
627
723
|
restoreTask(id) {
|
|
628
724
|
const { tasks, archived } = applyRestoreTask(this.tasks, id, this.now());
|
|
629
725
|
if (!archived) return false;
|
|
726
|
+
if (this.deps.transport !== void 0) {
|
|
727
|
+
this.commitRemote({
|
|
728
|
+
kind: "restore",
|
|
729
|
+
taskId: id
|
|
730
|
+
}, id).then((restored) => {
|
|
731
|
+
if (restored && this.selectedTaskId === id) this.closeTask();
|
|
732
|
+
});
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
630
735
|
this.tasks = [...tasks];
|
|
736
|
+
if (this.selectedTaskId === id) this.selectedTaskId = void 0;
|
|
631
737
|
this.persistAndNotify();
|
|
632
738
|
return true;
|
|
633
739
|
}
|
|
@@ -643,14 +749,21 @@ window.__ModuleLoader__.load({
|
|
|
643
749
|
setSchedule(id, patch) {
|
|
644
750
|
const { tasks, applied } = applySetSchedule(this.tasks, id, patch, this.now());
|
|
645
751
|
if (!applied) return false;
|
|
752
|
+
if (this.deps.transport !== void 0) {
|
|
753
|
+
this.commitRemote({
|
|
754
|
+
kind: "set-schedule",
|
|
755
|
+
taskId: id,
|
|
756
|
+
patch
|
|
757
|
+
}, id);
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
646
760
|
this.tasks = [...tasks];
|
|
647
761
|
this.persistAndNotify();
|
|
648
762
|
return true;
|
|
649
763
|
}
|
|
650
764
|
/**
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* schedule rule (it was deleted mid-tick, for example).
|
|
765
|
+
* Legacy pure-controller seam retained for migration-focused tests. The
|
|
766
|
+
* production browser never rolls schedules; the Host ledger owns them.
|
|
654
767
|
*/
|
|
655
768
|
applyScheduleNextRun(id, nextRunAt, lastTriggeredAt) {
|
|
656
769
|
const next = applyScheduleNextRun(this.tasks, id, nextRunAt, lastTriggeredAt, this.now());
|
|
@@ -658,12 +771,8 @@ window.__ModuleLoader__.load({
|
|
|
658
771
|
this.persistAndNotify();
|
|
659
772
|
}
|
|
660
773
|
/**
|
|
661
|
-
* Reload the
|
|
662
|
-
*
|
|
663
|
-
* tab (or a stale in-memory copy) can never be fired from this tab: the
|
|
664
|
-
* fire decision and the subsequent roll-forward both run on the freshest
|
|
665
|
-
* persisted truth. Deliberately silent — same-origin external changes still
|
|
666
|
-
* reach subscribers through the storage-event subscription.
|
|
774
|
+
* Reload the legacy v1 store without notifying subscribers. Production v2
|
|
775
|
+
* reads Host snapshots instead; this remains only for isolated legacy tests.
|
|
667
776
|
*/
|
|
668
777
|
reloadFromStore() {
|
|
669
778
|
this.tasks = this.deps.store.load();
|
|
@@ -683,11 +792,16 @@ window.__ModuleLoader__.load({
|
|
|
683
792
|
*/
|
|
684
793
|
async runTask(id) {
|
|
685
794
|
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
686
|
-
if (task === void 0 || task.status === "running") return false;
|
|
795
|
+
if (task === void 0 || task.archivedAt !== void 0 || task.status === "running") return false;
|
|
796
|
+
if (this.deps.transport !== void 0) return await this.commitRemote({
|
|
797
|
+
kind: "run",
|
|
798
|
+
taskId: id
|
|
799
|
+
}, id);
|
|
687
800
|
const { task: next, execution } = startExecution(task, this.now(), this.uuid());
|
|
688
801
|
this.tasks = this.tasks.map((candidate) => candidate.id === id ? next : candidate);
|
|
689
802
|
this.persistAndNotify();
|
|
690
803
|
this.activeExecutionIds.add(execution.id);
|
|
804
|
+
if (this.deps.exec === void 0) throw new Error("legacy execution service is unavailable");
|
|
691
805
|
await this.deps.exec.run(next, execution, (event) => {
|
|
692
806
|
this.handleExecutionEvent(event);
|
|
693
807
|
});
|
|
@@ -696,7 +810,14 @@ window.__ModuleLoader__.load({
|
|
|
696
810
|
/** Re-run a settled task: move it back to 'todo' first, then execute. */
|
|
697
811
|
async rerunTask(id) {
|
|
698
812
|
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
699
|
-
if (task === void 0) return;
|
|
813
|
+
if (task === void 0 || task.archivedAt !== void 0) return;
|
|
814
|
+
if (this.deps.transport !== void 0) {
|
|
815
|
+
await this.commitRemote({
|
|
816
|
+
kind: "rerun",
|
|
817
|
+
taskId: id
|
|
818
|
+
}, id);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
700
821
|
if (task.status !== "running") {
|
|
701
822
|
this.tasks = this.tasks.map((candidate) => candidate.id === id ? withStatus(candidate, "todo", this.now()) : candidate);
|
|
702
823
|
this.persistAndNotify();
|
|
@@ -715,7 +836,7 @@ window.__ModuleLoader__.load({
|
|
|
715
836
|
}
|
|
716
837
|
/** Reconcile running tasks and close the board when the user navigates. */
|
|
717
838
|
onSessionsChanged() {
|
|
718
|
-
this.scheduleReconcile();
|
|
839
|
+
if (this.deps.transport === void 0) this.scheduleReconcile();
|
|
719
840
|
if (!this.boardOpen) return;
|
|
720
841
|
const current = currentOf(this.deps.sessions);
|
|
721
842
|
if (current !== this.lastCurrent) this.closeBoard();
|
|
@@ -743,6 +864,7 @@ window.__ModuleLoader__.load({
|
|
|
743
864
|
}
|
|
744
865
|
/** Settle tasks left 'running' whose sessions already finished. */
|
|
745
866
|
async reconcileRunningTasks() {
|
|
867
|
+
if (this.deps.exec === void 0) return;
|
|
746
868
|
if (this.reconcileInFlight) return;
|
|
747
869
|
this.reconcileInFlight = true;
|
|
748
870
|
try {
|
|
@@ -773,434 +895,120 @@ window.__ModuleLoader__.load({
|
|
|
773
895
|
}
|
|
774
896
|
}
|
|
775
897
|
persistAndNotify() {
|
|
776
|
-
this.deps.store.save(this.tasks);
|
|
898
|
+
if (this.deps.transport === void 0) this.deps.store.save(this.tasks);
|
|
777
899
|
this.notify();
|
|
778
900
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
updatedAt: now,
|
|
788
|
-
executions: task.executions.map((execution) => execution.id === executionId ? {
|
|
789
|
-
...execution,
|
|
790
|
-
sessionId
|
|
791
|
-
} : execution)
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
//#endregion
|
|
795
|
-
//#region src/core/execution.ts
|
|
796
|
-
/** Human copy for a run failure. */
|
|
797
|
-
function messageOf(error) {
|
|
798
|
-
if (error instanceof Error) return error.message;
|
|
799
|
-
return String(error);
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* Whether a rejected preset switch actually means "the session already runs
|
|
803
|
-
* this preset" (the host's agent-preset-conflict with a matching
|
|
804
|
-
* existingPreset). A blank-session reuse race can produce this even though
|
|
805
|
-
* the requested composition is in place.
|
|
806
|
-
*/
|
|
807
|
-
function presetAlreadyRuns(error, mode) {
|
|
808
|
-
if (typeof error !== "object" || error === null) return false;
|
|
809
|
-
const details = error.details;
|
|
810
|
-
if (typeof details !== "object" || details === null) return false;
|
|
811
|
-
return details.existingPreset === mode;
|
|
812
|
-
}
|
|
813
|
-
/** Whether a `turn/end` payload closed the turn with an error reason. */
|
|
814
|
-
function isErrorTurnEnd(data) {
|
|
815
|
-
if (typeof data !== "object" || data === null) return false;
|
|
816
|
-
const reason = data.reason;
|
|
817
|
-
return typeof reason === "object" && reason !== null && reason.kind === "error";
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Run one task to completion (or to a settled failure).
|
|
821
|
-
*
|
|
822
|
-
* @param task - the task being executed.
|
|
823
|
-
* @param execution - the freshly opened execution record (id + start time).
|
|
824
|
-
* @param onEvent - callback for started/settled events.
|
|
825
|
-
* @returns resolves when the run settles (or fails to start); never rejects —
|
|
826
|
-
* every failure path is reported as a settled event.
|
|
827
|
-
*/
|
|
828
|
-
var ExecutionService = class {
|
|
829
|
-
env;
|
|
830
|
-
/**
|
|
831
|
-
* In-flight preset switches keyed by `sessionId\u0000mode`. The session
|
|
832
|
-
* list mirror can lag behind a just-applied switch, so concurrent runs
|
|
833
|
-
* against one blank session would otherwise each issue the same select
|
|
834
|
-
* RPC (an amplified storm); sharing one in-flight call keeps the wire to
|
|
835
|
-
* a single request. The settled entry is forgotten so a later run can
|
|
836
|
-
* switch again.
|
|
837
|
-
*/
|
|
838
|
-
presetSwitches = /* @__PURE__ */ new Map();
|
|
839
|
-
/** @param env - the runtime faces (real or fake). */
|
|
840
|
-
constructor(env) {
|
|
841
|
-
this.env = env;
|
|
842
|
-
}
|
|
843
|
-
async run(task, execution, onEvent) {
|
|
844
|
-
const settleFailed = (error) => {
|
|
845
|
-
onEvent({
|
|
846
|
-
kind: "settled",
|
|
847
|
-
taskId: task.id,
|
|
848
|
-
executionId: execution.id,
|
|
849
|
-
outcome: "failed",
|
|
850
|
-
error
|
|
851
|
-
});
|
|
852
|
-
};
|
|
901
|
+
async commitRemote(action, taskId) {
|
|
902
|
+
if (this.deps.transport === void 0) return true;
|
|
903
|
+
if (taskId === void 0) return await this.performRemote(action);
|
|
904
|
+
const operation = (this.taskQueues.get(taskId) ?? Promise.resolve()).catch(() => {}).then(async () => await this.performRemote(action));
|
|
905
|
+
const tail = operation.then(() => {}, () => {});
|
|
906
|
+
this.taskQueues.set(taskId, tail);
|
|
907
|
+
this.pendingTaskIds.add(taskId);
|
|
908
|
+
this.notify();
|
|
853
909
|
try {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
taskId
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
});
|
|
861
|
-
const driver = this.driverOf(sessionId);
|
|
862
|
-
if (driver === void 0) {
|
|
863
|
-
settleFailed("execution session is not ready");
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
if (!await this.applyMode(driver, task, sessionId, settleFailed)) return;
|
|
867
|
-
if (!await this.applyPermission(driver, task, settleFailed)) return;
|
|
868
|
-
await driver.rename(task.title).catch(() => {});
|
|
869
|
-
const baseline = driver.getSnapshot().turnEnds.size;
|
|
870
|
-
const accepted = await this.sendPrompt(driver, task);
|
|
871
|
-
if (!accepted.ok) {
|
|
872
|
-
settleFailed(messageOf(accepted.error));
|
|
873
|
-
return;
|
|
910
|
+
return await operation;
|
|
911
|
+
} finally {
|
|
912
|
+
if (this.taskQueues.get(taskId) === tail) {
|
|
913
|
+
this.taskQueues.delete(taskId);
|
|
914
|
+
this.pendingTaskIds.delete(taskId);
|
|
915
|
+
this.notify();
|
|
874
916
|
}
|
|
875
|
-
this.watchForSettlement(driver, task.id, execution.id, onEvent, baseline);
|
|
876
|
-
} catch (error) {
|
|
877
|
-
settleFailed(messageOf(error));
|
|
878
917
|
}
|
|
879
918
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
*/
|
|
886
|
-
async applyMode(driver, task, sessionId, settleFailed) {
|
|
887
|
-
const mode = task.mode;
|
|
888
|
-
if (mode === void 0 || mode === "") return true;
|
|
889
|
-
const summary = this.env.sessions.list.getSnapshot().byId[sessionId];
|
|
890
|
-
if (summary?.blank === false) {
|
|
891
|
-
settleFailed(`cannot switch agent preset to ${mode}: the execution session is not blank`);
|
|
892
|
-
return false;
|
|
893
|
-
}
|
|
894
|
-
if (summary?.agentPreset === mode) return true;
|
|
895
|
-
const presets = this.env.presets;
|
|
896
|
-
if (presets === void 0) {
|
|
897
|
-
settleFailed(`this deployment does not support agent presets (task asks for ${mode})`);
|
|
898
|
-
return false;
|
|
899
|
-
}
|
|
919
|
+
async performRemote(action) {
|
|
920
|
+
const transport = this.deps.transport;
|
|
921
|
+
if (transport === void 0) return true;
|
|
922
|
+
this.transportError = void 0;
|
|
923
|
+
this.notify();
|
|
900
924
|
try {
|
|
901
|
-
|
|
902
|
-
if (!result.ok) {
|
|
903
|
-
if (presetAlreadyRuns(result.error, mode)) {
|
|
904
|
-
this.env.sessions.noteAgentPreset?.(sessionId, mode);
|
|
905
|
-
return true;
|
|
906
|
-
}
|
|
907
|
-
settleFailed(`agent preset switch to ${mode} rejected: ${messageOf(result.error)}`);
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
925
|
+
return this.acceptRemote(await transport.action(action)) || await this.refreshRemote();
|
|
910
926
|
} catch (error) {
|
|
911
|
-
|
|
927
|
+
await this.refreshRemote(messageOf(error));
|
|
912
928
|
return false;
|
|
913
929
|
}
|
|
914
|
-
this.env.sessions.noteAgentPreset?.(sessionId, mode);
|
|
915
|
-
return true;
|
|
916
930
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
* run (after a shared failure, say) issues a fresh switch.
|
|
922
|
-
*/
|
|
923
|
-
switchPreset(presets, sessionId, mode) {
|
|
924
|
-
const key = `${sessionId}\u0000${mode}`;
|
|
925
|
-
const inflight = this.presetSwitches.get(key);
|
|
926
|
-
if (inflight !== void 0) return inflight;
|
|
927
|
-
const attempt = presets.select(sessionId, mode).finally(() => {
|
|
928
|
-
if (this.presetSwitches.get(key) === attempt) this.presetSwitches.delete(key);
|
|
929
|
-
});
|
|
930
|
-
this.presetSwitches.set(key, attempt);
|
|
931
|
-
return attempt;
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Apply the task-pinned permission preset through the `/permission <id>`
|
|
935
|
-
* slash command. No-op when the task pins none; fails the run when the
|
|
936
|
-
* admission is rejected or no command claimed the line.
|
|
937
|
-
*/
|
|
938
|
-
async applyPermission(driver, task, settleFailed) {
|
|
939
|
-
const permission = task.permission;
|
|
940
|
-
if (permission === void 0) return true;
|
|
941
|
-
const line = `/permission ${permission}`;
|
|
931
|
+
async initializeRemote() {
|
|
932
|
+
if (this.remoteInitialization !== void 0) return await this.remoteInitialization;
|
|
933
|
+
const initialization = this.doInitializeRemote();
|
|
934
|
+
this.remoteInitialization = initialization;
|
|
942
935
|
try {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
return false;
|
|
947
|
-
}
|
|
948
|
-
if (!result.matched) {
|
|
949
|
-
settleFailed(`permission command not recognized: ${line}`);
|
|
950
|
-
return false;
|
|
951
|
-
}
|
|
952
|
-
} catch (error) {
|
|
953
|
-
settleFailed(`permission command failed: ${messageOf(error)}`);
|
|
954
|
-
return false;
|
|
955
|
-
}
|
|
956
|
-
return true;
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Inspect a reloaded/background task that was left 'running' and emit a
|
|
960
|
-
* settled event when its session already finished.
|
|
961
|
-
*
|
|
962
|
-
* A session that was never opened keeps a cold conversation snapshot (the
|
|
963
|
-
* runtime only maintains the window for the staged/current session), so the
|
|
964
|
-
* settled outcome is decided by the strongest available signal, in order:
|
|
965
|
-
* 1. the list summary — missing session → cancelled; still running → pending;
|
|
966
|
-
* 2. a warm conversation snapshot → `lastAgentError` decides failed/succeeded;
|
|
967
|
-
* 3. the raw history tail (when a history face is wired) — a `turn/end`
|
|
968
|
-
* error reason proves failure;
|
|
969
|
-
* 4. otherwise a finished session counts as succeeded.
|
|
970
|
-
*
|
|
971
|
-
* @param task - a task whose latest execution has no endedAt.
|
|
972
|
-
* @returns a settled event when the session state proves completion, else undefined.
|
|
973
|
-
*/
|
|
974
|
-
async reconcile(task) {
|
|
975
|
-
const execution = task.executions[task.executions.length - 1];
|
|
976
|
-
if (execution === void 0 || execution.sessionId === void 0 || execution.endedAt !== void 0) return void 0;
|
|
977
|
-
const list = this.env.sessions.list.getSnapshot();
|
|
978
|
-
if (list.phase !== "ready") return void 0;
|
|
979
|
-
const summary = list.byId[execution.sessionId];
|
|
980
|
-
if (summary === void 0) return {
|
|
981
|
-
kind: "settled",
|
|
982
|
-
taskId: task.id,
|
|
983
|
-
executionId: execution.id,
|
|
984
|
-
outcome: "cancelled",
|
|
985
|
-
error: "execution session no longer exists"
|
|
986
|
-
};
|
|
987
|
-
if (summary.running) return void 0;
|
|
988
|
-
const driver = this.driverOf(execution.sessionId);
|
|
989
|
-
if (driver !== void 0) {
|
|
990
|
-
const snapshot = driver.getSnapshot();
|
|
991
|
-
if (snapshot.turnEnds.size > 0) {
|
|
992
|
-
const outcome = snapshot.lastAgentError !== null ? "failed" : "succeeded";
|
|
993
|
-
return {
|
|
994
|
-
kind: "settled",
|
|
995
|
-
taskId: task.id,
|
|
996
|
-
executionId: execution.id,
|
|
997
|
-
outcome,
|
|
998
|
-
error: snapshot.lastAgentError ?? void 0
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
936
|
+
return await initialization;
|
|
937
|
+
} finally {
|
|
938
|
+
if (this.remoteInitialization === initialization) this.remoteInitialization = void 0;
|
|
1001
939
|
}
|
|
1002
|
-
if (await this.historyShowsFailure(execution.sessionId)) return {
|
|
1003
|
-
kind: "settled",
|
|
1004
|
-
taskId: task.id,
|
|
1005
|
-
executionId: execution.id,
|
|
1006
|
-
outcome: "failed",
|
|
1007
|
-
error: "agent turn failed"
|
|
1008
|
-
};
|
|
1009
|
-
return {
|
|
1010
|
-
kind: "settled",
|
|
1011
|
-
taskId: task.id,
|
|
1012
|
-
executionId: execution.id,
|
|
1013
|
-
outcome: "succeeded"
|
|
1014
|
-
};
|
|
1015
940
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if (history === void 0) return false;
|
|
941
|
+
async doInitializeRemote() {
|
|
942
|
+
const transport = this.deps.transport;
|
|
943
|
+
if (transport === void 0) return true;
|
|
1020
944
|
try {
|
|
1021
|
-
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
945
|
+
this.acceptRemote(await transport.bootstrap(this.tasks));
|
|
946
|
+
if (!this.remoteSubscribed) {
|
|
947
|
+
this.remoteSubscribed = true;
|
|
948
|
+
this.disposers.push(transport.subscribe(() => {
|
|
949
|
+
this.refreshRemote();
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
return true;
|
|
1024
953
|
} catch (error) {
|
|
1025
|
-
|
|
954
|
+
this.transportError = messageOf(error);
|
|
955
|
+
this.notify();
|
|
1026
956
|
return false;
|
|
1027
957
|
}
|
|
1028
958
|
}
|
|
1029
|
-
async
|
|
1030
|
-
const
|
|
1031
|
-
if (
|
|
1032
|
-
if (!workspace.items.some((item) => item.workspaceId === taskWorkspaceId)) throw new Error(`task workspace is not available: ${taskWorkspaceId}`);
|
|
1033
|
-
return this.env.workspaces.connectWorkspace(taskWorkspaceId);
|
|
1034
|
-
}
|
|
1035
|
-
const workspaceId = workspace.recentWorkspaceId ?? workspace.items[0]?.workspaceId;
|
|
1036
|
-
if (workspaceId === void 0) throw new Error("no workspace available to run the task in");
|
|
1037
|
-
return this.env.workspaces.connectWorkspace(workspaceId);
|
|
1038
|
-
}
|
|
1039
|
-
driverOf(sessionId) {
|
|
1040
|
-
return this.env.sessions.binding(sessionId)?.session;
|
|
1041
|
-
}
|
|
1042
|
-
async sendPrompt(driver, task) {
|
|
1043
|
-
const text = task.prompt.trim() !== "" ? task.prompt : task.title;
|
|
959
|
+
async refreshRemote(preserveError) {
|
|
960
|
+
const transport = this.deps.transport;
|
|
961
|
+
if (transport === void 0) return true;
|
|
1044
962
|
try {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
963
|
+
this.acceptRemote(await transport.state());
|
|
964
|
+
if (preserveError !== void 0) {
|
|
965
|
+
this.transportError = preserveError;
|
|
966
|
+
this.notify();
|
|
967
|
+
}
|
|
968
|
+
return true;
|
|
1049
969
|
} catch (error) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
};
|
|
970
|
+
this.transportError = preserveError ?? messageOf(error);
|
|
971
|
+
this.notify();
|
|
972
|
+
return false;
|
|
1054
973
|
}
|
|
1055
974
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
let settled = false;
|
|
1064
|
-
let unsubscribe = () => {};
|
|
1065
|
-
const check = () => {
|
|
1066
|
-
if (settled) return;
|
|
1067
|
-
const snapshot = driver.getSnapshot();
|
|
1068
|
-
if (snapshot.running || snapshot.turnEnds.size <= baseline) return;
|
|
1069
|
-
settled = true;
|
|
1070
|
-
unsubscribe();
|
|
1071
|
-
onEvent({
|
|
1072
|
-
kind: "settled",
|
|
1073
|
-
taskId,
|
|
1074
|
-
executionId,
|
|
1075
|
-
outcome: snapshot.lastAgentError !== null ? "failed" : "succeeded",
|
|
1076
|
-
error: snapshot.lastAgentError ?? void 0
|
|
1077
|
-
});
|
|
975
|
+
acceptRemote(snapshot) {
|
|
976
|
+
if (this.hostState?.scheduler.ledgerId === snapshot.scheduler.ledgerId && this.hostState !== void 0 && snapshot.revision < this.hostState.revision) return false;
|
|
977
|
+
this.tasks = [...snapshot.tasks];
|
|
978
|
+
this.hostState = {
|
|
979
|
+
revision: snapshot.revision,
|
|
980
|
+
scheduler: snapshot.scheduler,
|
|
981
|
+
power: snapshot.power
|
|
1078
982
|
};
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
//#region src/core/scheduler.ts
|
|
1085
|
-
/**
|
|
1086
|
-
* Browser-side scheduler: the heartbeat behind scheduled task runs.
|
|
1087
|
-
*
|
|
1088
|
-
* The board is a pure client plugin with no server channel, so "定时任务"
|
|
1089
|
-
* lives in the tab: a timer ticks every minute (plus immediately on tab
|
|
1090
|
-
* visibility recovery) and triggers any task whose `schedule.nextRunAt` is
|
|
1091
|
-
* due, rolling the schedule forward to the next cron match before triggering
|
|
1092
|
-
* so the same tick never double-fires. Missed runs are skipped, never queued:
|
|
1093
|
-
* a task still running at its due instant is skipped by the controller's
|
|
1094
|
-
* runTask guard and simply waits for the next cron match.
|
|
1095
|
-
*
|
|
1096
|
-
* A deleted task can never fire again from a stale in-memory copy: every
|
|
1097
|
-
* tick first re-reads the persisted ledger through the optional `refresh`
|
|
1098
|
-
* hook (the controller reloads its store), so the fire decision and the
|
|
1099
|
-
* subsequent roll-forward both run on the freshest persisted truth.
|
|
1100
|
-
*
|
|
1101
|
-
* The ticker is controlled: `start` arms a single interval guarded by an
|
|
1102
|
-
* idempotence check (a second start while running is a no-op, so wiring can
|
|
1103
|
-
* never leak a duplicate timer), and `stop`/`dispose` clear the timer and
|
|
1104
|
-
* drop the environment listeners. The board unmount path calls stop/dispose,
|
|
1105
|
-
* so no interval or listener outlives the board.
|
|
1106
|
-
*
|
|
1107
|
-
* Framework-free: all runtime access flows through the injected deps
|
|
1108
|
-
* (structural faces), so tests drive ticks directly without timers.
|
|
1109
|
-
*/
|
|
1110
|
-
/**
|
|
1111
|
-
* The schedule heartbeat (see module doc). `tick` is public so tests and
|
|
1112
|
-
* callers can drive a check without waiting for the interval.
|
|
1113
|
-
*/
|
|
1114
|
-
var SchedulerService = class {
|
|
1115
|
-
deps;
|
|
1116
|
-
timer;
|
|
1117
|
-
environmentListener;
|
|
1118
|
-
disposed = false;
|
|
1119
|
-
started = false;
|
|
1120
|
-
/** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
|
|
1121
|
-
constructor(deps) {
|
|
1122
|
-
this.deps = deps;
|
|
1123
|
-
}
|
|
1124
|
-
/**
|
|
1125
|
-
* Start ticking: one immediate check (catch-up after reload) + the interval.
|
|
1126
|
-
* Single-instance: a second start while already armed is a no-op, so a
|
|
1127
|
-
* re-entrant mount can never stack a duplicate timer.
|
|
1128
|
-
*/
|
|
1129
|
-
start() {
|
|
1130
|
-
if (this.disposed) return;
|
|
1131
|
-
if (this.started) return;
|
|
1132
|
-
this.started = true;
|
|
1133
|
-
this.tick();
|
|
1134
|
-
this.timer = setInterval(() => {
|
|
1135
|
-
this.tick();
|
|
1136
|
-
}, this.deps.tickMs ?? 6e4);
|
|
1137
|
-
if (this.deps.environment !== void 0) {
|
|
1138
|
-
this.environmentListener = () => {
|
|
1139
|
-
this.tick();
|
|
1140
|
-
};
|
|
1141
|
-
this.deps.environment.addEventListener("visibilitychange", this.environmentListener);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Stop ticking and drop listeners (idempotent). Preferred shutdown verb for
|
|
1146
|
-
* callers that treat the scheduler as a controlled ticker; `dispose` is an
|
|
1147
|
-
* alias.
|
|
1148
|
-
*/
|
|
1149
|
-
stop() {
|
|
1150
|
-
this.dispose();
|
|
1151
|
-
}
|
|
1152
|
-
/** Stop ticking and drop listeners (idempotent). */
|
|
1153
|
-
dispose() {
|
|
1154
|
-
if (this.disposed && this.timer === void 0 && this.environmentListener === void 0) return;
|
|
1155
|
-
this.disposed = true;
|
|
1156
|
-
this.started = false;
|
|
1157
|
-
if (this.timer !== void 0) {
|
|
1158
|
-
clearInterval(this.timer);
|
|
1159
|
-
this.timer = void 0;
|
|
1160
|
-
}
|
|
1161
|
-
if (this.environmentListener !== void 0 && this.deps.environment !== void 0) {
|
|
1162
|
-
this.deps.environment.removeEventListener("visibilitychange", this.environmentListener);
|
|
1163
|
-
this.environmentListener = void 0;
|
|
1164
|
-
}
|
|
983
|
+
this.transportError = void 0;
|
|
984
|
+
if (this.selectedTaskId !== void 0 && !this.tasks.some((task) => task.id === this.selectedTaskId)) this.selectedTaskId = void 0;
|
|
985
|
+
if (!this.archiveView && this.selectedTaskId !== void 0 && this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
|
|
986
|
+
this.notify();
|
|
987
|
+
return true;
|
|
1165
988
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
* task per tick: the schedule is rolled forward only after runTask accepts
|
|
1169
|
-
* the run, so a rejected run keeps its due slot and is retried on the next
|
|
1170
|
-
* tick instead of being silently dropped.
|
|
1171
|
-
*/
|
|
1172
|
-
async tick() {
|
|
1173
|
-
if (this.disposed) return;
|
|
1174
|
-
if (this.deps.ready !== void 0 && !this.deps.ready()) return;
|
|
1175
|
-
this.deps.refresh?.();
|
|
1176
|
-
const now = this.deps.now();
|
|
1177
|
-
for (const task of this.deps.tasks()) {
|
|
1178
|
-
const schedule = task.schedule;
|
|
1179
|
-
if (schedule === void 0 || !schedule.enabled) continue;
|
|
1180
|
-
if (schedule.nextRunAt === void 0) {
|
|
1181
|
-
const repaired = nextRunAtMs(schedule.cron, now);
|
|
1182
|
-
if (repaired === void 0) continue;
|
|
1183
|
-
this.deps.applySchedule(task.id, repaired, void 0);
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
if (schedule.nextRunAt > now) continue;
|
|
1187
|
-
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
|
|
1188
|
-
if (await this.deps.runTask(task.id)) this.deps.applySchedule(task.id, next, now);
|
|
1189
|
-
}
|
|
989
|
+
notify() {
|
|
990
|
+
for (const fn of [...this.listeners]) fn();
|
|
1190
991
|
}
|
|
1191
992
|
};
|
|
993
|
+
/** Record which session ran an execution (once the execution service reports it). */
|
|
994
|
+
function attachSessionId(task, executionId, sessionId, now) {
|
|
995
|
+
return {
|
|
996
|
+
...task,
|
|
997
|
+
updatedAt: now,
|
|
998
|
+
executions: task.executions.map((execution) => execution.id === executionId ? {
|
|
999
|
+
...execution,
|
|
1000
|
+
sessionId
|
|
1001
|
+
} : execution)
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1192
1004
|
//#endregion
|
|
1193
1005
|
//#region src/core/store.ts
|
|
1194
1006
|
/**
|
|
1195
|
-
*
|
|
1007
|
+
* Legacy v1 browser persistence and the store seam used by pure client tests.
|
|
1196
1008
|
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
* localStorage under a versioned key — the same persistence mechanism dsh's
|
|
1201
|
-
* own client snapshot stores use (`createSnapshotStore` persist). Data
|
|
1202
|
-
* survives page refreshes and dsh restarts (same origin), and survives
|
|
1203
|
-
* plugin uninstall (the key is simply left in place).
|
|
1009
|
+
* Production v2 state is Host-authoritative. This backend is retained only to
|
|
1010
|
+
* read `dsh.taskBoard.v1` for one-time import; the old value is never removed,
|
|
1011
|
+
* so it remains a read-only rollback copy after migration.
|
|
1204
1012
|
*
|
|
1205
1013
|
* The seam keeps the backend swappable (e.g. an IndexedDB or a host-file
|
|
1206
1014
|
* channel later); tests run against the in-memory backend and a jsdom
|
|
@@ -1239,10 +1047,6 @@ window.__ModuleLoader__.load({
|
|
|
1239
1047
|
}
|
|
1240
1048
|
return true;
|
|
1241
1049
|
}
|
|
1242
|
-
/** Collapse a blank persisted target string to undefined (clears the pin). */
|
|
1243
|
-
function normalizeTargetId(value) {
|
|
1244
|
-
return value !== void 0 && value.trim() === "" ? void 0 : value;
|
|
1245
|
-
}
|
|
1246
1050
|
/** Normalize an unknown persisted status back into the closed status union. */
|
|
1247
1051
|
function normalizeStatus(status) {
|
|
1248
1052
|
return isTaskStatus(status) ? status : "todo";
|
|
@@ -1402,8 +1206,12 @@ window.__ModuleLoader__.load({
|
|
|
1402
1206
|
"board.status.done": "已完成",
|
|
1403
1207
|
"board.status.failed": "已失败",
|
|
1404
1208
|
"board.runs": "次执行",
|
|
1209
|
+
"board.pending": "正在提交",
|
|
1405
1210
|
"board.updated": "更新于",
|
|
1406
1211
|
"board.created": "创建于",
|
|
1212
|
+
"board.hostError": "Host 操作失败:{error}",
|
|
1213
|
+
"board.retryHost": "重试连接 Host",
|
|
1214
|
+
"board.hostMeta": "Host 时区 {timeZone} · revision {revision}",
|
|
1407
1215
|
"new.title": "标题",
|
|
1408
1216
|
"new.titlePlaceholder": "一句话描述要做什么",
|
|
1409
1217
|
"new.description": "描述",
|
|
@@ -1472,11 +1280,17 @@ window.__ModuleLoader__.load({
|
|
|
1472
1280
|
"detail.executionSettings": "执行设置",
|
|
1473
1281
|
"exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。",
|
|
1474
1282
|
"settings.title": "任务看板",
|
|
1475
|
-
"settings.description": "
|
|
1283
|
+
"settings.description": "控制 Host 任务看板、agent 播报与运行期间的系统空闲睡眠保护。",
|
|
1476
1284
|
"settings.enabled": "启用任务看板",
|
|
1477
1285
|
"settings.enabledHint": "关闭后隐藏侧边栏入口与看板视图。",
|
|
1478
1286
|
"settings.announceToAgent": "向 agent 播报任务看板",
|
|
1479
1287
|
"settings.announceToAgentHint": "开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报,agent 仅在用户主动提及时了解看板。",
|
|
1288
|
+
"settings.preventIdleSleep": "阻止系统空闲睡眠",
|
|
1289
|
+
"settings.preventIdleSleepHint": "默认关闭。开启后,只要存在运行中的 DSH 会话、已启用的定时任务或会话状态尚未确认,Host 就阻止整机因空闲睡眠;屏幕仍可自动关闭。",
|
|
1290
|
+
"settings.powerStatus": "平台:{platform};保护状态:{phase};运行会话:{running};已启用计划:{schedules}",
|
|
1291
|
+
"settings.powerBoundary": "这可能增加电池消耗。合盖、手动睡眠、休眠、关机、低电量或企业策略不在保证范围内,也不会唤醒已经睡眠的机器。",
|
|
1292
|
+
"settings.powerUnknown": "未知",
|
|
1293
|
+
"settings.powerError": "最近一次电源保护错误:{error}",
|
|
1480
1294
|
"settings.inherit": "继承",
|
|
1481
1295
|
"settings.on": "开",
|
|
1482
1296
|
"settings.off": "关",
|
|
@@ -1513,8 +1327,12 @@ window.__ModuleLoader__.load({
|
|
|
1513
1327
|
"board.status.done": "Done",
|
|
1514
1328
|
"board.status.failed": "Failed",
|
|
1515
1329
|
"board.runs": "runs",
|
|
1330
|
+
"board.pending": "Submitting",
|
|
1516
1331
|
"board.updated": "Updated",
|
|
1517
1332
|
"board.created": "Created",
|
|
1333
|
+
"board.hostError": "Host action failed: {error}",
|
|
1334
|
+
"board.retryHost": "Retry Host connection",
|
|
1335
|
+
"board.hostMeta": "Host time zone {timeZone} · revision {revision}",
|
|
1518
1336
|
"new.title": "Title",
|
|
1519
1337
|
"new.titlePlaceholder": "What should be done, in one line",
|
|
1520
1338
|
"new.description": "Description",
|
|
@@ -1583,11 +1401,17 @@ window.__ModuleLoader__.load({
|
|
|
1583
1401
|
"detail.executionSettings": "Execution Settings",
|
|
1584
1402
|
"exec.hint": "Applied when the task runs: the workspace decides where the execution session lands; the mode composes the session's agent preset; the permission is applied through the /permission command. Blank = runtime default.",
|
|
1585
1403
|
"settings.title": "Task Board",
|
|
1586
|
-
"settings.description": "
|
|
1404
|
+
"settings.description": "Configure the Host task board, agent announcement, and idle-system-sleep protection while work is pending.",
|
|
1587
1405
|
"settings.enabled": "Enable the task board",
|
|
1588
1406
|
"settings.enabledHint": "When off, the sidebar entry and board view are hidden.",
|
|
1589
1407
|
"settings.announceToAgent": "Announce the task board to agents",
|
|
1590
1408
|
"settings.announceToAgentHint": "On: every agent system prompt includes a note about this board. Off: no announcement; agents learn about the board only when you mention it.",
|
|
1409
|
+
"settings.preventIdleSleep": "Prevent idle system sleep",
|
|
1410
|
+
"settings.preventIdleSleepHint": "Off by default. When enabled, the Host prevents idle system sleep while any DSH session runs, any schedule is enabled, or session state is not yet known. The display may still turn off.",
|
|
1411
|
+
"settings.powerStatus": "Platform: {platform}; protection: {phase}; running sessions: {running}; enabled schedules: {schedules}",
|
|
1412
|
+
"settings.powerBoundary": "This may use more battery. Lid close, manual sleep, hibernation, shutdown, low-battery actions, and enterprise policy are outside the guarantee; an already sleeping computer is not woken.",
|
|
1413
|
+
"settings.powerUnknown": "unknown",
|
|
1414
|
+
"settings.powerError": "Latest power-protection error: {error}",
|
|
1591
1415
|
"settings.inherit": "Inherit",
|
|
1592
1416
|
"settings.on": "On",
|
|
1593
1417
|
"settings.off": "Off",
|
|
@@ -1717,7 +1541,7 @@ window.__ModuleLoader__.load({
|
|
|
1717
1541
|
//#region src/client/board/NewTaskModal.tsx
|
|
1718
1542
|
/**
|
|
1719
1543
|
* New-task modal: title + description + the prompt that execution will send.
|
|
1720
|
-
* Creates through the
|
|
1544
|
+
* Creates through the Host and closes only after the Host confirms it.
|
|
1721
1545
|
*/
|
|
1722
1546
|
/** New-task form overlay. */
|
|
1723
1547
|
function NewTaskModal({ controller, onClose }) {
|
|
@@ -1731,9 +1555,10 @@ window.__ModuleLoader__.load({
|
|
|
1731
1555
|
const [scheduleCron, setScheduleCron] = (0, react.useState)("");
|
|
1732
1556
|
const [scheduleError, setScheduleError] = (0, react.useState)(void 0);
|
|
1733
1557
|
const [error, setError] = (0, react.useState)(void 0);
|
|
1558
|
+
const [pending, setPending] = (0, react.useState)(false);
|
|
1734
1559
|
const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
|
|
1735
1560
|
(0, react.useEffect)(() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)), [controller]);
|
|
1736
|
-
const submit = () => {
|
|
1561
|
+
const submit = async () => {
|
|
1737
1562
|
if (scheduleEnabled) {
|
|
1738
1563
|
const cron = scheduleCron.trim();
|
|
1739
1564
|
if (cron === "" || !isValidCron(cron)) {
|
|
@@ -1741,7 +1566,8 @@ window.__ModuleLoader__.load({
|
|
|
1741
1566
|
return;
|
|
1742
1567
|
}
|
|
1743
1568
|
}
|
|
1744
|
-
|
|
1569
|
+
setPending(true);
|
|
1570
|
+
if (await controller.createTaskConfirmed({
|
|
1745
1571
|
title,
|
|
1746
1572
|
description,
|
|
1747
1573
|
prompt,
|
|
@@ -1753,7 +1579,8 @@ window.__ModuleLoader__.load({
|
|
|
1753
1579
|
cron: scheduleCron.trim()
|
|
1754
1580
|
} : void 0
|
|
1755
1581
|
}) === void 0) {
|
|
1756
|
-
|
|
1582
|
+
setPending(false);
|
|
1583
|
+
setError(controller.getSnapshot().transportError ?? t("new.required"));
|
|
1757
1584
|
return;
|
|
1758
1585
|
}
|
|
1759
1586
|
onClose();
|
|
@@ -1964,6 +1791,7 @@ window.__ModuleLoader__.load({
|
|
|
1964
1791
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1965
1792
|
type: "submit",
|
|
1966
1793
|
className: board_module_css_default.primaryButton,
|
|
1794
|
+
disabled: pending,
|
|
1967
1795
|
children: t("new.submit")
|
|
1968
1796
|
})]
|
|
1969
1797
|
})
|
|
@@ -1972,6 +1800,16 @@ window.__ModuleLoader__.load({
|
|
|
1972
1800
|
});
|
|
1973
1801
|
}
|
|
1974
1802
|
//#endregion
|
|
1803
|
+
//#region src/client/board/status-key.ts
|
|
1804
|
+
/** Task status → locale key (board column titles and the detail badge). */
|
|
1805
|
+
const STATUS_KEY = {
|
|
1806
|
+
backlog: "board.status.backlog",
|
|
1807
|
+
todo: "board.status.todo",
|
|
1808
|
+
running: "board.status.running",
|
|
1809
|
+
done: "board.status.done",
|
|
1810
|
+
failed: "board.status.failed"
|
|
1811
|
+
};
|
|
1812
|
+
//#endregion
|
|
1975
1813
|
//#region src/client/board/TaskCard.tsx
|
|
1976
1814
|
/**
|
|
1977
1815
|
* Task card: the board's column item. Clicking opens the task detail — it
|
|
@@ -1983,22 +1821,36 @@ window.__ModuleLoader__.load({
|
|
|
1983
1821
|
* by the board, so the memo boundary is effective.
|
|
1984
1822
|
*/
|
|
1985
1823
|
/** Compact relative/absolute time label. */
|
|
1986
|
-
function
|
|
1824
|
+
function formatHostTimestamp(ms, timeZone) {
|
|
1825
|
+
try {
|
|
1826
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
1827
|
+
dateStyle: "medium",
|
|
1828
|
+
timeStyle: "medium",
|
|
1829
|
+
...timeZone === void 0 ? {} : { timeZone }
|
|
1830
|
+
}).format(new Date(ms));
|
|
1831
|
+
} catch {
|
|
1832
|
+
return new Date(ms).toISOString();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function formatTime(ms, timeZone) {
|
|
1987
1836
|
const date = new Date(ms);
|
|
1988
1837
|
const minutes = Math.floor((Date.now() - ms) / 6e4);
|
|
1989
1838
|
if (minutes < 1) return t("time.justNow");
|
|
1990
1839
|
if (minutes < 60) return `${minutes}m`;
|
|
1991
1840
|
if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
|
|
1841
|
+
if (timeZone !== void 0) return formatHostTimestamp(ms, timeZone);
|
|
1992
1842
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
1993
1843
|
}
|
|
1994
1844
|
/** One card in a column. */
|
|
1995
|
-
function TaskCardInner({ task, onClick }) {
|
|
1845
|
+
function TaskCardInner({ task, pending, timeZone, onClick }) {
|
|
1996
1846
|
const latest = task.executions[task.executions.length - 1];
|
|
1997
1847
|
const runs = task.executions.length;
|
|
1848
|
+
const archived = task.archivedAt !== void 0;
|
|
1998
1849
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1999
1850
|
type: "button",
|
|
2000
1851
|
className: board_module_css_default.card,
|
|
2001
|
-
"data-status": task.status,
|
|
1852
|
+
"data-status": archived ? "archived" : task.status,
|
|
1853
|
+
"data-pending": pending || void 0,
|
|
2002
1854
|
onClick,
|
|
2003
1855
|
title: task.description !== "" ? task.description : task.title,
|
|
2004
1856
|
children: [
|
|
@@ -2021,14 +1873,14 @@ window.__ModuleLoader__.load({
|
|
|
2021
1873
|
formatTime(task.updatedAt)
|
|
2022
1874
|
]
|
|
2023
1875
|
}),
|
|
2024
|
-
task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1876
|
+
!archived && task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2025
1877
|
className: board_module_css_default.cardSchedule,
|
|
2026
|
-
title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${
|
|
1878
|
+
title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}` : t("card.scheduled"),
|
|
2027
1879
|
children: t("card.scheduled")
|
|
2028
1880
|
}),
|
|
2029
1881
|
latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2030
1882
|
className: board_module_css_default.cardRun,
|
|
2031
|
-
"data-result": latest.result,
|
|
1883
|
+
"data-result": archived ? void 0 : latest.result,
|
|
2032
1884
|
children: [
|
|
2033
1885
|
runs,
|
|
2034
1886
|
" ",
|
|
@@ -2040,13 +1892,17 @@ window.__ModuleLoader__.load({
|
|
|
2040
1892
|
title: latest.sessionId,
|
|
2041
1893
|
children: "⌁"
|
|
2042
1894
|
}),
|
|
2043
|
-
task.status === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1895
|
+
!archived && (task.status === "running" || pending) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2044
1896
|
className: board_module_css_default.cardSpinner,
|
|
2045
1897
|
"aria-hidden": "true"
|
|
2046
1898
|
})
|
|
2047
1899
|
]
|
|
2048
1900
|
}),
|
|
2049
|
-
|
|
1901
|
+
!archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1902
|
+
className: board_module_css_default.cardRunningLabel,
|
|
1903
|
+
children: [t("board.pending"), "…"]
|
|
1904
|
+
}),
|
|
1905
|
+
!archived && latest !== void 0 && executionLabel(latest) === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2050
1906
|
className: board_module_css_default.cardRunningLabel,
|
|
2051
1907
|
children: [t("detail.result.running"), "…"]
|
|
2052
1908
|
})
|
|
@@ -2112,16 +1968,8 @@ window.__ModuleLoader__.load({
|
|
|
2112
1968
|
failed: "detail.result.failed",
|
|
2113
1969
|
cancelled: "detail.result.cancelled"
|
|
2114
1970
|
};
|
|
2115
|
-
/** Status → locale key (detail badge). */
|
|
2116
|
-
const STATUS_KEY$1 = {
|
|
2117
|
-
backlog: "board.status.backlog",
|
|
2118
|
-
todo: "board.status.todo",
|
|
2119
|
-
running: "board.status.running",
|
|
2120
|
-
done: "board.status.done",
|
|
2121
|
-
failed: "board.status.failed"
|
|
2122
|
-
};
|
|
2123
1971
|
/** One execution-history row. */
|
|
2124
|
-
function ExecutionRow({ execution, onOpen }) {
|
|
1972
|
+
function ExecutionRow({ execution, timeZone, onOpen }) {
|
|
2125
1973
|
const result = execution.result;
|
|
2126
1974
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2127
1975
|
className: board_module_css_default.executionRow,
|
|
@@ -2137,8 +1985,8 @@ window.__ModuleLoader__.load({
|
|
|
2137
1985
|
children: [
|
|
2138
1986
|
t("detail.executionStarted"),
|
|
2139
1987
|
" ",
|
|
2140
|
-
formatTime(execution.startedAt),
|
|
2141
|
-
execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt)}`
|
|
1988
|
+
formatTime(execution.startedAt, timeZone),
|
|
1989
|
+
execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt, timeZone)}`
|
|
2142
1990
|
]
|
|
2143
1991
|
}),
|
|
2144
1992
|
execution.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -2158,7 +2006,7 @@ window.__ModuleLoader__.load({
|
|
|
2158
2006
|
});
|
|
2159
2007
|
}
|
|
2160
2008
|
/** The execution-target editor: workspace / mode / permission pickers. */
|
|
2161
|
-
function ExecutionSettingsSection({ controller, task }) {
|
|
2009
|
+
function ExecutionSettingsSection({ controller, task, pending }) {
|
|
2162
2010
|
const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
|
|
2163
2011
|
(0, react.useEffect)(() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)), [controller]);
|
|
2164
2012
|
const workspaceId = task.workspaceId ?? "";
|
|
@@ -2182,6 +2030,7 @@ window.__ModuleLoader__.load({
|
|
|
2182
2030
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2183
2031
|
className: board_module_css_default.select,
|
|
2184
2032
|
value: workspaceId,
|
|
2033
|
+
disabled: pending,
|
|
2185
2034
|
onChange: (event) => {
|
|
2186
2035
|
controller.updateTask(task.id, { workspaceId: event.target.value });
|
|
2187
2036
|
},
|
|
@@ -2209,6 +2058,7 @@ window.__ModuleLoader__.load({
|
|
|
2209
2058
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2210
2059
|
className: board_module_css_default.select,
|
|
2211
2060
|
value: mode,
|
|
2061
|
+
disabled: pending,
|
|
2212
2062
|
onChange: (event) => {
|
|
2213
2063
|
controller.updateTask(task.id, { mode: event.target.value });
|
|
2214
2064
|
},
|
|
@@ -2241,6 +2091,7 @@ window.__ModuleLoader__.load({
|
|
|
2241
2091
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2242
2092
|
className: board_module_css_default.select,
|
|
2243
2093
|
value: permission,
|
|
2094
|
+
disabled: pending,
|
|
2244
2095
|
onChange: (event) => {
|
|
2245
2096
|
controller.updateTask(task.id, { permission: event.target.value === "" ? void 0 : event.target.value });
|
|
2246
2097
|
},
|
|
@@ -2257,13 +2108,14 @@ window.__ModuleLoader__.load({
|
|
|
2257
2108
|
});
|
|
2258
2109
|
}
|
|
2259
2110
|
/** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
|
|
2260
|
-
function ScheduleSection({ controller, task }) {
|
|
2111
|
+
function ScheduleSection({ controller, task, pending }) {
|
|
2261
2112
|
const schedule = task.schedule;
|
|
2262
2113
|
const [cron, setCron] = (0, react.useState)(schedule?.cron ?? "0 9 * * *");
|
|
2263
2114
|
const [enabled, setEnabled] = (0, react.useState)(schedule?.enabled ?? false);
|
|
2264
2115
|
const [nextRunAt, setNextRunAt] = (0, react.useState)(schedule?.nextRunAt);
|
|
2265
2116
|
const [lastTriggeredAt, setLastTriggeredAt] = (0, react.useState)(schedule?.lastTriggeredAt);
|
|
2266
2117
|
const [error, setError] = (0, react.useState)(void 0);
|
|
2118
|
+
const timeZone = controller.getSnapshot().host?.scheduler.timeZone;
|
|
2267
2119
|
(0, react.useEffect)(() => {
|
|
2268
2120
|
setCron(schedule?.cron ?? "0 9 * * *");
|
|
2269
2121
|
setEnabled(schedule?.enabled ?? false);
|
|
@@ -2296,8 +2148,10 @@ window.__ModuleLoader__.load({
|
|
|
2296
2148
|
return;
|
|
2297
2149
|
}
|
|
2298
2150
|
setError(void 0);
|
|
2299
|
-
if (
|
|
2300
|
-
|
|
2151
|
+
if (controller.setSchedule(task.id, {
|
|
2152
|
+
enabled: next,
|
|
2153
|
+
...next && trimmed !== schedule?.cron ? { cron: trimmed } : {}
|
|
2154
|
+
}) && !controller.isHostBacked()) setEnabled(next);
|
|
2301
2155
|
};
|
|
2302
2156
|
const applyPreset = (preset) => {
|
|
2303
2157
|
if (preset === "") return;
|
|
@@ -2305,8 +2159,8 @@ window.__ModuleLoader__.load({
|
|
|
2305
2159
|
setError(void 0);
|
|
2306
2160
|
controller.setSchedule(task.id, { cron: preset });
|
|
2307
2161
|
};
|
|
2308
|
-
const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= Date.now() ? t("detail.schedule.dueSoon") :
|
|
2309
|
-
const lastLabel = lastTriggeredAt === void 0 ? "—" :
|
|
2162
|
+
const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= Date.now() ? t("detail.schedule.dueSoon") : formatHostTimestamp(nextRunAt, timeZone);
|
|
2163
|
+
const lastLabel = lastTriggeredAt === void 0 ? "—" : formatHostTimestamp(lastTriggeredAt, timeZone);
|
|
2310
2164
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2311
2165
|
className: board_module_css_default.detailSection,
|
|
2312
2166
|
children: [
|
|
@@ -2316,6 +2170,7 @@ window.__ModuleLoader__.load({
|
|
|
2316
2170
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2317
2171
|
type: "checkbox",
|
|
2318
2172
|
checked: enabled,
|
|
2173
|
+
disabled: pending,
|
|
2319
2174
|
onChange: (event) => {
|
|
2320
2175
|
toggleEnabled(event.target.checked);
|
|
2321
2176
|
}
|
|
@@ -2326,6 +2181,7 @@ window.__ModuleLoader__.load({
|
|
|
2326
2181
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2327
2182
|
className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${error !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
|
|
2328
2183
|
value: cron,
|
|
2184
|
+
disabled: pending,
|
|
2329
2185
|
placeholder: "0 9 * * *",
|
|
2330
2186
|
spellCheck: false,
|
|
2331
2187
|
"aria-label": t("detail.schedule.cron"),
|
|
@@ -2342,6 +2198,7 @@ window.__ModuleLoader__.load({
|
|
|
2342
2198
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2343
2199
|
className: board_module_css_default.schedulePreset,
|
|
2344
2200
|
value: "",
|
|
2201
|
+
disabled: pending,
|
|
2345
2202
|
"aria-label": t("detail.schedule.presets"),
|
|
2346
2203
|
onChange: (event) => {
|
|
2347
2204
|
applyPreset(event.target.value);
|
|
@@ -2377,12 +2234,17 @@ window.__ModuleLoader__.load({
|
|
|
2377
2234
|
/** Task detail overlay. */
|
|
2378
2235
|
function TaskDetail({ controller, task }) {
|
|
2379
2236
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
2380
|
-
const running = task.status === "running";
|
|
2381
2237
|
const [latest, setLatest] = (0, react.useState)(task);
|
|
2382
2238
|
(0, react.useEffect)(() => {
|
|
2383
2239
|
setLatest(task);
|
|
2384
2240
|
}, [task]);
|
|
2385
2241
|
const current = latest;
|
|
2242
|
+
const snapshot = controller.getSnapshot();
|
|
2243
|
+
const running = current.status === "running";
|
|
2244
|
+
const archived = current.archivedAt !== void 0;
|
|
2245
|
+
const pending = snapshot.pendingTaskIds.includes(current.id);
|
|
2246
|
+
const transportError = snapshot.transportError;
|
|
2247
|
+
const timeZone = snapshot.host?.scheduler.timeZone;
|
|
2386
2248
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2387
2249
|
className: board_module_css_default.modalBackdrop,
|
|
2388
2250
|
onMouseDown: (event) => {
|
|
@@ -2402,8 +2264,8 @@ window.__ModuleLoader__.load({
|
|
|
2402
2264
|
}),
|
|
2403
2265
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2404
2266
|
className: board_module_css_default.statusBadge,
|
|
2405
|
-
"data-status": current.status,
|
|
2406
|
-
children: t(STATUS_KEY
|
|
2267
|
+
"data-status": archived ? "archived" : current.status,
|
|
2268
|
+
children: archived ? t("board.archive") : t(STATUS_KEY[current.status])
|
|
2407
2269
|
}),
|
|
2408
2270
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2409
2271
|
type: "button",
|
|
@@ -2419,6 +2281,21 @@ window.__ModuleLoader__.load({
|
|
|
2419
2281
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2420
2282
|
className: board_module_css_default.detailBody,
|
|
2421
2283
|
children: [
|
|
2284
|
+
transportError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2285
|
+
className: board_module_css_default.formError,
|
|
2286
|
+
children: [
|
|
2287
|
+
t("board.hostError", { error: transportError }),
|
|
2288
|
+
" ",
|
|
2289
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2290
|
+
type: "button",
|
|
2291
|
+
className: board_module_css_default.linkButton,
|
|
2292
|
+
onClick: () => {
|
|
2293
|
+
controller.retryHostSync();
|
|
2294
|
+
},
|
|
2295
|
+
children: t("board.retryHost")
|
|
2296
|
+
})
|
|
2297
|
+
]
|
|
2298
|
+
}),
|
|
2422
2299
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2423
2300
|
className: board_module_css_default.detailSection,
|
|
2424
2301
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.description") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -2433,14 +2310,15 @@ window.__ModuleLoader__.load({
|
|
|
2433
2310
|
children: current.prompt !== "" ? current.prompt : current.title
|
|
2434
2311
|
})]
|
|
2435
2312
|
}),
|
|
2436
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
|
|
2313
|
+
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
|
|
2437
2314
|
controller,
|
|
2438
|
-
task: current
|
|
2439
|
-
|
|
2440
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
|
|
2315
|
+
task: current,
|
|
2316
|
+
pending
|
|
2317
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
|
|
2441
2318
|
controller,
|
|
2442
|
-
task: current
|
|
2443
|
-
|
|
2319
|
+
task: current,
|
|
2320
|
+
pending
|
|
2321
|
+
})] }),
|
|
2444
2322
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2445
2323
|
className: board_module_css_default.detailSection,
|
|
2446
2324
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.execution") }), current.executions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -2450,20 +2328,21 @@ window.__ModuleLoader__.load({
|
|
|
2450
2328
|
className: board_module_css_default.executionList,
|
|
2451
2329
|
children: [...current.executions].reverse().map((execution) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionRow, {
|
|
2452
2330
|
execution,
|
|
2331
|
+
timeZone,
|
|
2453
2332
|
onOpen: (sessionId) => {
|
|
2454
2333
|
controller.openSession(sessionId);
|
|
2455
2334
|
}
|
|
2456
2335
|
}, execution.id))
|
|
2457
2336
|
})]
|
|
2458
2337
|
}),
|
|
2459
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2338
|
+
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2460
2339
|
className: board_module_css_default.detailSection,
|
|
2461
2340
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("board.status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2462
2341
|
className: board_module_css_default.moveRow,
|
|
2463
2342
|
children: MANUAL_STATUSES.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2464
2343
|
type: "button",
|
|
2465
2344
|
className: board_module_css_default.ghostButton,
|
|
2466
|
-
disabled: current.status === status || running,
|
|
2345
|
+
disabled: current.status === status || running || pending,
|
|
2467
2346
|
onClick: () => {
|
|
2468
2347
|
controller.moveTask(current.id, status);
|
|
2469
2348
|
},
|
|
@@ -2476,36 +2355,42 @@ window.__ModuleLoader__.load({
|
|
|
2476
2355
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
2477
2356
|
className: board_module_css_default.detailFooter,
|
|
2478
2357
|
children: [
|
|
2479
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2358
|
+
!archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2359
|
+
className: board_module_css_default.detailMeta,
|
|
2360
|
+
children: [t("board.pending"), "…"]
|
|
2361
|
+
}),
|
|
2362
|
+
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2480
2363
|
type: "button",
|
|
2481
2364
|
className: board_module_css_default.primaryButton,
|
|
2482
|
-
disabled: running,
|
|
2365
|
+
disabled: running || pending,
|
|
2483
2366
|
onClick: () => {
|
|
2484
|
-
controller.
|
|
2485
|
-
|
|
2367
|
+
controller.rerunTask(current.id).then(() => {
|
|
2368
|
+
if (controller.getSnapshot().transportError === void 0) controller.closeTask();
|
|
2369
|
+
});
|
|
2486
2370
|
},
|
|
2487
2371
|
children: current.executions.length === 0 ? t("detail.run") : t("detail.rerun")
|
|
2488
2372
|
}),
|
|
2489
|
-
|
|
2373
|
+
archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2490
2374
|
type: "button",
|
|
2491
2375
|
className: board_module_css_default.primaryButton,
|
|
2376
|
+
disabled: pending,
|
|
2492
2377
|
onClick: () => {
|
|
2493
2378
|
controller.restoreTask(current.id);
|
|
2494
|
-
controller.closeTask();
|
|
2495
2379
|
},
|
|
2496
2380
|
children: t("detail.restore")
|
|
2497
2381
|
}) : (current.status === "done" || current.status === "failed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2498
2382
|
type: "button",
|
|
2499
2383
|
className: board_module_css_default.ghostButton,
|
|
2384
|
+
disabled: pending,
|
|
2500
2385
|
onClick: () => {
|
|
2501
2386
|
controller.archiveTask(current.id);
|
|
2502
|
-
controller.closeTask();
|
|
2503
2387
|
},
|
|
2504
2388
|
children: t("detail.archive")
|
|
2505
2389
|
}),
|
|
2506
2390
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2507
2391
|
type: "button",
|
|
2508
2392
|
className: board_module_css_default.dangerButton,
|
|
2393
|
+
disabled: pending,
|
|
2509
2394
|
onClick: () => {
|
|
2510
2395
|
setConfirmDelete(true);
|
|
2511
2396
|
},
|
|
@@ -2516,8 +2401,8 @@ window.__ModuleLoader__.load({
|
|
|
2516
2401
|
children: [
|
|
2517
2402
|
t("board.created"),
|
|
2518
2403
|
" ",
|
|
2519
|
-
formatTime(current.createdAt),
|
|
2520
|
-
|
|
2404
|
+
formatTime(current.createdAt, timeZone),
|
|
2405
|
+
archived && ` · ${t("detail.archivedAt", { time: formatTime(current.archivedAt, timeZone) })}`
|
|
2521
2406
|
]
|
|
2522
2407
|
})
|
|
2523
2408
|
]
|
|
@@ -2534,7 +2419,6 @@ window.__ModuleLoader__.load({
|
|
|
2534
2419
|
onConfirm: () => {
|
|
2535
2420
|
setConfirmDelete(false);
|
|
2536
2421
|
controller.deleteTask(current.id);
|
|
2537
|
-
controller.closeTask();
|
|
2538
2422
|
}
|
|
2539
2423
|
})]
|
|
2540
2424
|
});
|
|
@@ -2546,14 +2430,6 @@ window.__ModuleLoader__.load({
|
|
|
2546
2430
|
* active. Cards open the task detail (never execute directly); the header
|
|
2547
2431
|
* offers filter, new-task, and a back-to-chat escape.
|
|
2548
2432
|
*/
|
|
2549
|
-
/** Column status → locale key. */
|
|
2550
|
-
const STATUS_KEY = {
|
|
2551
|
-
backlog: "board.status.backlog",
|
|
2552
|
-
todo: "board.status.todo",
|
|
2553
|
-
running: "board.status.running",
|
|
2554
|
-
done: "board.status.done",
|
|
2555
|
-
failed: "board.status.failed"
|
|
2556
|
-
};
|
|
2557
2433
|
/** Case-insensitive title/description match. */
|
|
2558
2434
|
function matchesFilter(task, filter) {
|
|
2559
2435
|
if (filter.trim() === "") return true;
|
|
@@ -2566,9 +2442,11 @@ window.__ModuleLoader__.load({
|
|
|
2566
2442
|
* re-renders only when its own task changes — not when a sibling card status,
|
|
2567
2443
|
* the filter, or the selection moves.
|
|
2568
2444
|
*/
|
|
2569
|
-
const MemoTaskCard = (0, react.memo)(function MemoTaskCard({ task, onOpen }) {
|
|
2445
|
+
const MemoTaskCard = (0, react.memo)(function MemoTaskCard({ task, pending, timeZone, onOpen }) {
|
|
2570
2446
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
2571
2447
|
task,
|
|
2448
|
+
pending,
|
|
2449
|
+
timeZone,
|
|
2572
2450
|
onClick: (0, react.useCallback)(() => {
|
|
2573
2451
|
onOpen(task.id);
|
|
2574
2452
|
}, [task.id, onOpen])
|
|
@@ -2609,6 +2487,13 @@ window.__ModuleLoader__.load({
|
|
|
2609
2487
|
className: board_module_css_default.boardTitle,
|
|
2610
2488
|
children: t("board.title")
|
|
2611
2489
|
}),
|
|
2490
|
+
snapshot.host !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2491
|
+
className: board_module_css_default.detailMeta,
|
|
2492
|
+
children: t("board.hostMeta", {
|
|
2493
|
+
revision: String(snapshot.host.revision),
|
|
2494
|
+
timeZone: snapshot.host.scheduler.timeZone
|
|
2495
|
+
})
|
|
2496
|
+
}),
|
|
2612
2497
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2613
2498
|
className: board_module_css_default.search,
|
|
2614
2499
|
type: "search",
|
|
@@ -2637,6 +2522,21 @@ window.__ModuleLoader__.load({
|
|
|
2637
2522
|
})
|
|
2638
2523
|
]
|
|
2639
2524
|
}),
|
|
2525
|
+
snapshot.transportError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2526
|
+
className: board_module_css_default.formError,
|
|
2527
|
+
children: [
|
|
2528
|
+
t("board.hostError", { error: snapshot.transportError }),
|
|
2529
|
+
" ",
|
|
2530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2531
|
+
type: "button",
|
|
2532
|
+
className: board_module_css_default.linkButton,
|
|
2533
|
+
onClick: () => {
|
|
2534
|
+
controller.retryHostSync();
|
|
2535
|
+
},
|
|
2536
|
+
children: t("board.retryHost")
|
|
2537
|
+
})
|
|
2538
|
+
]
|
|
2539
|
+
}),
|
|
2640
2540
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2641
2541
|
className: board_module_css_default.columns,
|
|
2642
2542
|
children: archiveView ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
@@ -2655,6 +2555,8 @@ window.__ModuleLoader__.load({
|
|
|
2655
2555
|
className: board_module_css_default.cards,
|
|
2656
2556
|
children: [visible.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoTaskCard, {
|
|
2657
2557
|
task,
|
|
2558
|
+
pending: snapshot.pendingTaskIds.includes(task.id),
|
|
2559
|
+
timeZone: snapshot.host?.scheduler.timeZone,
|
|
2658
2560
|
onOpen: openTask
|
|
2659
2561
|
}, task.id)), visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2660
2562
|
className: board_module_css_default.columnEmpty,
|
|
@@ -2687,6 +2589,8 @@ window.__ModuleLoader__.load({
|
|
|
2687
2589
|
className: board_module_css_default.cards,
|
|
2688
2590
|
children: [tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoTaskCard, {
|
|
2689
2591
|
task,
|
|
2592
|
+
pending: snapshot.pendingTaskIds.includes(task.id),
|
|
2593
|
+
timeZone: snapshot.host?.scheduler.timeZone,
|
|
2690
2594
|
onOpen: openTask
|
|
2691
2595
|
}, task.id)), tasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2692
2596
|
className: board_module_css_default.columnEmpty,
|
|
@@ -3044,7 +2948,7 @@ window.__ModuleLoader__.load({
|
|
|
3044
2948
|
children: props.t("settings.readOnly")
|
|
3045
2949
|
}) : null,
|
|
3046
2950
|
props.children,
|
|
3047
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2951
|
+
props.hideFooter === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3048
2952
|
className: settings_card_module_css_default.footer,
|
|
3049
2953
|
children: [
|
|
3050
2954
|
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
@@ -3390,14 +3294,19 @@ window.__ModuleLoader__.load({
|
|
|
3390
3294
|
store;
|
|
3391
3295
|
/** @param scope - the bound settings scope for the `task-board` namespace. */
|
|
3392
3296
|
constructor(scope) {
|
|
3393
|
-
this.form = new CardForm(scope, [
|
|
3297
|
+
this.form = new CardForm(scope, [
|
|
3298
|
+
booleanField("enabled"),
|
|
3299
|
+
booleanField("announceToAgent"),
|
|
3300
|
+
booleanField("preventIdleSleep")
|
|
3301
|
+
]);
|
|
3394
3302
|
this.store = this.form.bind(() => this.projection());
|
|
3395
3303
|
}
|
|
3396
3304
|
projection() {
|
|
3397
3305
|
return {
|
|
3398
3306
|
...this.form.shell(),
|
|
3399
3307
|
enabled: this.form.field("enabled"),
|
|
3400
|
-
announceToAgent: this.form.field("announceToAgent")
|
|
3308
|
+
announceToAgent: this.form.field("announceToAgent"),
|
|
3309
|
+
preventIdleSleep: this.form.field("preventIdleSleep")
|
|
3401
3310
|
};
|
|
3402
3311
|
}
|
|
3403
3312
|
/**
|
|
@@ -3427,6 +3336,29 @@ window.__ModuleLoader__.load({
|
|
|
3427
3336
|
const { t } = props;
|
|
3428
3337
|
const state = props.useTaskBoardSettingsCard((snapshot) => snapshot);
|
|
3429
3338
|
const disabled = !state.writable;
|
|
3339
|
+
const [power, setPower] = (0, react.useState)();
|
|
3340
|
+
(0, react.useEffect)(() => {
|
|
3341
|
+
let live = true;
|
|
3342
|
+
const refresh = async () => {
|
|
3343
|
+
try {
|
|
3344
|
+
const response = await fetch("/api/task-board/state", {
|
|
3345
|
+
cache: "no-store",
|
|
3346
|
+
credentials: "same-origin"
|
|
3347
|
+
});
|
|
3348
|
+
if (!response.ok) return;
|
|
3349
|
+
const snapshot = await response.json();
|
|
3350
|
+
if (live) setPower(snapshot.power);
|
|
3351
|
+
} catch {}
|
|
3352
|
+
};
|
|
3353
|
+
refresh();
|
|
3354
|
+
const timer = setInterval(() => {
|
|
3355
|
+
refresh();
|
|
3356
|
+
}, 5e3);
|
|
3357
|
+
return () => {
|
|
3358
|
+
live = false;
|
|
3359
|
+
clearInterval(timer);
|
|
3360
|
+
};
|
|
3361
|
+
}, []);
|
|
3430
3362
|
const fieldProps = {
|
|
3431
3363
|
overriddenLabel: t("settings.overridden"),
|
|
3432
3364
|
resetLabel: t("settings.reset"),
|
|
@@ -3437,43 +3369,166 @@ window.__ModuleLoader__.load({
|
|
|
3437
3369
|
t,
|
|
3438
3370
|
titleKey: "settings.title",
|
|
3439
3371
|
descriptionKey: "settings.description",
|
|
3372
|
+
defaultOpen: false,
|
|
3440
3373
|
state,
|
|
3441
3374
|
onSave: props.save,
|
|
3442
3375
|
onDiscard: props.discard,
|
|
3443
|
-
children: [
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3376
|
+
children: [
|
|
3377
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
3378
|
+
id: "settings-task-board-enabled",
|
|
3379
|
+
label: t("settings.enabled"),
|
|
3380
|
+
hint: t("settings.enabledHint"),
|
|
3381
|
+
inheritLabel: t("settings.inherit"),
|
|
3382
|
+
onLabel: t("settings.on"),
|
|
3383
|
+
offLabel: t("settings.off"),
|
|
3384
|
+
...fieldProps,
|
|
3385
|
+
...state.enabled,
|
|
3386
|
+
onEdit: (text) => {
|
|
3387
|
+
props.edit("enabled", text);
|
|
3388
|
+
},
|
|
3389
|
+
onReset: () => {
|
|
3390
|
+
props.resetField("enabled");
|
|
3391
|
+
}
|
|
3392
|
+
}),
|
|
3393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
3394
|
+
id: "settings-task-board-announce",
|
|
3395
|
+
label: t("settings.announceToAgent"),
|
|
3396
|
+
hint: t("settings.announceToAgentHint"),
|
|
3397
|
+
inheritLabel: t("settings.inherit"),
|
|
3398
|
+
onLabel: t("settings.on"),
|
|
3399
|
+
offLabel: t("settings.off"),
|
|
3400
|
+
...fieldProps,
|
|
3401
|
+
...state.announceToAgent,
|
|
3402
|
+
onEdit: (text) => {
|
|
3403
|
+
props.edit("announceToAgent", text);
|
|
3404
|
+
},
|
|
3405
|
+
onReset: () => {
|
|
3406
|
+
props.resetField("announceToAgent");
|
|
3407
|
+
}
|
|
3408
|
+
}),
|
|
3409
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
3410
|
+
id: "settings-task-board-prevent-idle-sleep",
|
|
3411
|
+
label: t("settings.preventIdleSleep"),
|
|
3412
|
+
hint: t("settings.preventIdleSleepHint"),
|
|
3413
|
+
inheritLabel: t("settings.inherit"),
|
|
3414
|
+
onLabel: t("settings.on"),
|
|
3415
|
+
offLabel: t("settings.off"),
|
|
3416
|
+
...fieldProps,
|
|
3417
|
+
...state.preventIdleSleep,
|
|
3418
|
+
onEdit: (text) => {
|
|
3419
|
+
props.edit("preventIdleSleep", text);
|
|
3420
|
+
},
|
|
3421
|
+
onReset: () => {
|
|
3422
|
+
props.resetField("preventIdleSleep");
|
|
3423
|
+
}
|
|
3424
|
+
}),
|
|
3425
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.powerStatus", {
|
|
3426
|
+
platform: power?.platform ?? t("settings.powerUnknown"),
|
|
3427
|
+
phase: power?.phase ?? t("settings.powerUnknown"),
|
|
3428
|
+
running: String(power?.runningSessions ?? 0),
|
|
3429
|
+
schedules: String(power?.armedSchedules ?? 0)
|
|
3430
|
+
}) }),
|
|
3431
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.powerBoundary") }),
|
|
3432
|
+
power?.lastError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.powerError", { error: power.lastError }) })
|
|
3433
|
+
]
|
|
3474
3434
|
});
|
|
3475
3435
|
}
|
|
3476
3436
|
//#endregion
|
|
3437
|
+
//#region src/protocol.ts
|
|
3438
|
+
const TASK_BOARD_API_PREFIX = "/api/task-board";
|
|
3439
|
+
//#endregion
|
|
3440
|
+
//#region src/client/host-api.ts
|
|
3441
|
+
const IMPORT_MARKER = "dsh.taskBoard.v2.hostImported";
|
|
3442
|
+
const SOURCE_KEY = "dsh.taskBoard.v2.sourceId";
|
|
3443
|
+
const IMPORT_REQUEST_KEY = "dsh.taskBoard.v2.importRequestId";
|
|
3444
|
+
const REQUEST_TIMEOUT_MS = 15e3;
|
|
3445
|
+
function uuid() {
|
|
3446
|
+
return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
3447
|
+
}
|
|
3448
|
+
async function readJson(response) {
|
|
3449
|
+
const body = await response.json();
|
|
3450
|
+
if (!response.ok) throw new Error(body.error ?? `task-board request failed: ${response.status}`);
|
|
3451
|
+
return body;
|
|
3452
|
+
}
|
|
3453
|
+
var HttpTaskBoardHostTransport = class {
|
|
3454
|
+
storage;
|
|
3455
|
+
constructor(storage = globalThis.localStorage) {
|
|
3456
|
+
this.storage = storage;
|
|
3457
|
+
}
|
|
3458
|
+
async bootstrap(legacy) {
|
|
3459
|
+
const initial = await this.state();
|
|
3460
|
+
const ledgerId = initial.scheduler.ledgerId;
|
|
3461
|
+
if (legacy.length > 0 && ledgerId !== void 0 && this.storage?.getItem(IMPORT_MARKER) !== ledgerId) {
|
|
3462
|
+
let sourceId = this.storage?.getItem(SOURCE_KEY);
|
|
3463
|
+
if (sourceId === null || sourceId === void 0 || sourceId === "") {
|
|
3464
|
+
sourceId = uuid();
|
|
3465
|
+
this.storage?.setItem(SOURCE_KEY, sourceId);
|
|
3466
|
+
}
|
|
3467
|
+
let requestId = this.storage?.getItem(IMPORT_REQUEST_KEY);
|
|
3468
|
+
if (requestId === null || requestId === void 0 || requestId === "") {
|
|
3469
|
+
requestId = uuid();
|
|
3470
|
+
this.storage?.setItem(IMPORT_REQUEST_KEY, requestId);
|
|
3471
|
+
}
|
|
3472
|
+
const snapshot = await this.post(requestId, {
|
|
3473
|
+
kind: "import",
|
|
3474
|
+
sourceId,
|
|
3475
|
+
tasks: [...legacy]
|
|
3476
|
+
});
|
|
3477
|
+
this.storage?.setItem(IMPORT_MARKER, snapshot.scheduler.ledgerId ?? ledgerId);
|
|
3478
|
+
return snapshot;
|
|
3479
|
+
}
|
|
3480
|
+
return initial;
|
|
3481
|
+
}
|
|
3482
|
+
async state() {
|
|
3483
|
+
return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
|
|
3484
|
+
}
|
|
3485
|
+
async action(action) {
|
|
3486
|
+
return await this.post(uuid(), action);
|
|
3487
|
+
}
|
|
3488
|
+
async post(requestId, action) {
|
|
3489
|
+
const envelope = {
|
|
3490
|
+
requestId,
|
|
3491
|
+
action
|
|
3492
|
+
};
|
|
3493
|
+
return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
|
|
3494
|
+
method: "POST",
|
|
3495
|
+
headers: { "content-type": "application/json" },
|
|
3496
|
+
body: JSON.stringify(envelope)
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
async request(url, init) {
|
|
3500
|
+
const controller = new AbortController();
|
|
3501
|
+
const timeout = globalThis.setTimeout(() => {
|
|
3502
|
+
controller.abort();
|
|
3503
|
+
}, REQUEST_TIMEOUT_MS);
|
|
3504
|
+
try {
|
|
3505
|
+
return await readJson(await fetch(url, {
|
|
3506
|
+
...init,
|
|
3507
|
+
signal: controller.signal
|
|
3508
|
+
}));
|
|
3509
|
+
} catch (error) {
|
|
3510
|
+
if (controller.signal.aborted) throw new Error(`task-board Host request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
|
|
3511
|
+
throw error;
|
|
3512
|
+
} finally {
|
|
3513
|
+
globalThis.clearTimeout(timeout);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
subscribe(listener) {
|
|
3517
|
+
const events = new EventSource(`${TASK_BOARD_API_PREFIX}/events`);
|
|
3518
|
+
events.onmessage = () => {
|
|
3519
|
+
listener();
|
|
3520
|
+
};
|
|
3521
|
+
const onVisible = () => {
|
|
3522
|
+
if (document.visibilityState === "visible") listener();
|
|
3523
|
+
};
|
|
3524
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
3525
|
+
return () => {
|
|
3526
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
3527
|
+
events.close();
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
//#endregion
|
|
3477
3532
|
//#region src/client/index.ts
|
|
3478
3533
|
/** Locale namespace this plugin owns. */
|
|
3479
3534
|
const NS = "task-board";
|
|
@@ -3518,85 +3573,18 @@ window.__ModuleLoader__.load({
|
|
|
3518
3573
|
let uiDisposer;
|
|
3519
3574
|
const mountUi = () => {
|
|
3520
3575
|
if (uiDisposer !== void 0) return;
|
|
3521
|
-
const sessions = ctx.sessions;
|
|
3522
|
-
const workspaces = ctx.workspaces;
|
|
3576
|
+
const sessions = ctx.get("sessions");
|
|
3577
|
+
const workspaces = ctx.get("workspaces");
|
|
3523
3578
|
const connection = ctx.get("connection");
|
|
3524
3579
|
const controller = new BoardController({
|
|
3525
3580
|
store: new LocalStorageTaskStore(),
|
|
3526
|
-
|
|
3527
|
-
sessions: {
|
|
3528
|
-
list: sessions.list,
|
|
3529
|
-
binding: (id) => {
|
|
3530
|
-
const binding = sessions.binding(id);
|
|
3531
|
-
if (binding === void 0) return void 0;
|
|
3532
|
-
const { session } = binding;
|
|
3533
|
-
return { session: {
|
|
3534
|
-
rename: (title) => session.rename(title),
|
|
3535
|
-
prompt: (content, mode) => session.prompt(content, mode).then((result) => result.ok ? { ok: true } : {
|
|
3536
|
-
ok: false,
|
|
3537
|
-
error: result.error
|
|
3538
|
-
}),
|
|
3539
|
-
command: (line) => session.command(line).then((result) => result.ok ? {
|
|
3540
|
-
ok: true,
|
|
3541
|
-
matched: result.value.matched
|
|
3542
|
-
} : {
|
|
3543
|
-
ok: false,
|
|
3544
|
-
error: result.error
|
|
3545
|
-
}),
|
|
3546
|
-
getSnapshot: () => session.getSnapshot(),
|
|
3547
|
-
subscribe: (fn) => session.subscribe(fn)
|
|
3548
|
-
} };
|
|
3549
|
-
},
|
|
3550
|
-
noteAgentPreset: (sessionId, agentPreset) => sessions.noteAgentPreset(sessionId, agentPreset)
|
|
3551
|
-
},
|
|
3552
|
-
workspaces: {
|
|
3553
|
-
list: workspaces.list,
|
|
3554
|
-
connectWorkspace: (id) => workspaces.connectWorkspace(id)
|
|
3555
|
-
},
|
|
3556
|
-
presets: { select: async (sessionId, agentPreset) => {
|
|
3557
|
-
try {
|
|
3558
|
-
const response = await connection.api.agentPresets.select({
|
|
3559
|
-
sessionId,
|
|
3560
|
-
agentPreset
|
|
3561
|
-
});
|
|
3562
|
-
return response.result.ok ? { ok: true } : {
|
|
3563
|
-
ok: false,
|
|
3564
|
-
error: response.result.error
|
|
3565
|
-
};
|
|
3566
|
-
} catch (error) {
|
|
3567
|
-
return {
|
|
3568
|
-
ok: false,
|
|
3569
|
-
error
|
|
3570
|
-
};
|
|
3571
|
-
}
|
|
3572
|
-
} },
|
|
3573
|
-
history: { loadTail: async (sessionId) => {
|
|
3574
|
-
const response = await connection.api.sessions.history({
|
|
3575
|
-
sessionId,
|
|
3576
|
-
maxMessages: 20
|
|
3577
|
-
});
|
|
3578
|
-
return response.result.ok ? { events: response.result.value.events.map((entry) => entry.event) } : void 0;
|
|
3579
|
-
} }
|
|
3580
|
-
}),
|
|
3581
|
+
transport: new HttpTaskBoardHostTransport(),
|
|
3581
3582
|
sessions: {
|
|
3582
3583
|
list: sessions.list,
|
|
3583
3584
|
open: (id) => sessions.open(id)
|
|
3584
3585
|
}
|
|
3585
3586
|
});
|
|
3586
3587
|
controller.start();
|
|
3587
|
-
const scheduler = new SchedulerService({
|
|
3588
|
-
tasks: () => controller.getSnapshot().tasks,
|
|
3589
|
-
refresh: () => controller.reloadFromStore(),
|
|
3590
|
-
now: () => Date.now(),
|
|
3591
|
-
runTask: (id) => controller.runTask(id),
|
|
3592
|
-
applySchedule: (id, nextRunAt, lastTriggeredAt) => controller.applyScheduleNextRun(id, nextRunAt, lastTriggeredAt),
|
|
3593
|
-
ready: () => sessions.list.getSnapshot().phase === "ready",
|
|
3594
|
-
environment: {
|
|
3595
|
-
addEventListener: (type, listener) => document.addEventListener(type, listener),
|
|
3596
|
-
removeEventListener: (type, listener) => document.removeEventListener(type, listener)
|
|
3597
|
-
}
|
|
3598
|
-
});
|
|
3599
|
-
scheduler.start();
|
|
3600
3588
|
const disposers = [];
|
|
3601
3589
|
const pushWorkspaceOptions = () => {
|
|
3602
3590
|
const snapshot = workspaces.list.getSnapshot();
|
|
@@ -3634,7 +3622,6 @@ window.__ModuleLoader__.load({
|
|
|
3634
3622
|
}
|
|
3635
3623
|
uiDisposer = () => {
|
|
3636
3624
|
for (const dispose of disposers.splice(0)) dispose();
|
|
3637
|
-
scheduler.dispose();
|
|
3638
3625
|
controller.dispose();
|
|
3639
3626
|
uiDisposer = void 0;
|
|
3640
3627
|
};
|