@linxin666/dsh-client-ui-task-board 0.1.20 → 0.2.1
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 +83 -105
- package/README.zh.md +83 -105
- package/lib/client.js +902 -633
- package/lib/client.js.map +1 -1
- package/lib/index.js +2274 -5
- package/lib/types/client/PluginSettingsCard.d.ts +29 -1
- package/lib/types/client/TaskBoardSettingsCard.d.ts +7 -4
- package/lib/types/client/board/TaskCard.d.ts +5 -2
- package/lib/types/client/host-api.d.ts +18 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/sidebar-entry-core.d.ts +60 -0
- package/lib/types/client/sidebar-entry.d.ts +8 -10
- package/lib/types/core/controller.d.ts +47 -15
- package/lib/types/core/schedule.d.ts +11 -2
- package/lib/types/core/tasks.d.ts +6 -6
- 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 +52 -0
- package/lib/types/host-routes.d.ts +26 -0
- package/lib/types/host-runner.d.ts +41 -0
- package/lib/types/host-service.d.ts +45 -0
- package/lib/types/index.d.ts +17 -9
- package/lib/types/loopback.d.ts +24 -0
- package/lib/types/power-inhibitor.d.ts +73 -0
- package/lib/types/protocol.d.ts +78 -0
- package/package.json +12 -9
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/TaskBoardSettingsCard.tsx +51 -4
- package/src/client/board/NewTaskModal.tsx +9 -6
- package/src/client/board/TaskBoard.tsx +24 -7
- package/src/client/board/TaskCard.tsx +25 -8
- package/src/client/board/TaskDetail.tsx +83 -50
- package/src/client/board-mount.tsx +1 -0
- package/src/client/host-api.ts +104 -0
- package/src/client/index.ts +9 -78
- package/src/client/locales.ts +24 -4
- package/src/client/settings-card.module.css +90 -1
- package/src/client/sidebar-entry-core.ts +208 -0
- package/src/client/sidebar-entry.ts +24 -144
- package/src/core/controller.ts +222 -24
- package/src/core/schedule.ts +77 -13
- package/src/core/store.ts +4 -8
- package/src/core/tasks.ts +6 -6
- package/src/core/use-cases/task-archive.ts +14 -5
- package/src/core/use-cases/task-schedule.ts +6 -5
- package/src/dsh-home.ts +36 -0
- package/src/host-ledger.ts +539 -0
- package/src/host-routes.ts +196 -0
- package/src/host-runner.ts +152 -0
- package/src/host-service.ts +226 -0
- package/src/index.ts +54 -11
- package/src/loopback.ts +63 -0
- package/src/power-inhibitor.ts +321 -0
- package/src/protocol.ts +220 -0
- package/lib/types/core/scheduler.d.ts +0 -70
- package/src/core/scheduler.ts +0 -161
package/lib/client.js
CHANGED
|
@@ -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,19 +256,91 @@ 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.
|
|
263
|
+
*
|
|
264
|
+
* Walks candidate year/month/day/hour/minute values straight from the parsed
|
|
265
|
+
* field sets instead of scanning every minute: a sparse expression such as
|
|
266
|
+
* `0 0 29 2 *` used to iterate ~1.5M wall-clock minutes before reaching the
|
|
267
|
+
* next leap day. Wall-clock field construction + the final `matches` re-check
|
|
268
|
+
* preserve the old minute scan's DST semantics exactly (nonexistent spring
|
|
269
|
+
* minutes normalize forward and the repeated fall-back hour is never visited).
|
|
254
270
|
*/
|
|
255
271
|
function nextRunAtMs(expr, fromMs) {
|
|
256
272
|
const schedule = parseCron(expr);
|
|
257
273
|
if (schedule === null) return void 0;
|
|
274
|
+
if (!hasPossibleCalendarDay(schedule)) return void 0;
|
|
258
275
|
const from = new Date(fromMs);
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
276
|
+
const limitMs = fromMs + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
277
|
+
const sortedMinutes = [...schedule.minutes].sort((a, b) => a - b);
|
|
278
|
+
const sortedHours = [...schedule.hours].sort((a, b) => a - b);
|
|
279
|
+
const sortedMonths = [...schedule.months].sort((a, b) => a - b);
|
|
280
|
+
let year = from.getFullYear();
|
|
281
|
+
let month = from.getMonth() + 1;
|
|
282
|
+
let day = from.getDate();
|
|
283
|
+
let hour = from.getHours();
|
|
284
|
+
let minute = from.getMinutes() + 1;
|
|
285
|
+
while (new Date(year, month - 1, 1, 0, 0, 0, 0).getTime() <= limitMs) {
|
|
286
|
+
for (const candidateMonth of sortedMonths) {
|
|
287
|
+
if (candidateMonth < month) continue;
|
|
288
|
+
const daysInMonth = new Date(year, candidateMonth, 0).getDate();
|
|
289
|
+
const dayStart = candidateMonth === month ? day : 1;
|
|
290
|
+
for (let candidateDay = dayStart; candidateDay <= daysInMonth; candidateDay += 1) {
|
|
291
|
+
if (!dayCandidate(schedule, new Date(year, candidateMonth - 1, candidateDay, 0, 0, 0, 0))) continue;
|
|
292
|
+
const hourStart = candidateMonth === month && candidateDay === day ? hour : 0;
|
|
293
|
+
for (const candidateHour of sortedHours) {
|
|
294
|
+
if (candidateHour < hourStart) continue;
|
|
295
|
+
const minuteStart = candidateMonth === month && candidateDay === day && candidateHour === hour ? minute : 0;
|
|
296
|
+
for (const candidateMinute of sortedMinutes) {
|
|
297
|
+
if (candidateMinute < minuteStart) continue;
|
|
298
|
+
const candidate = new Date(year, candidateMonth - 1, candidateDay, candidateHour, candidateMinute, 0, 0);
|
|
299
|
+
const time = candidate.getTime();
|
|
300
|
+
if (time <= fromMs) continue;
|
|
301
|
+
if (time > limitMs) return void 0;
|
|
302
|
+
if (matches(schedule, candidate)) return time;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
year += 1;
|
|
308
|
+
month = 1;
|
|
309
|
+
day = 1;
|
|
310
|
+
hour = 0;
|
|
311
|
+
minute = 0;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** Day/weekday OR gate shared by {@link matches} and the candidate scan. */
|
|
315
|
+
function dayCandidate(schedule, date) {
|
|
316
|
+
const dayMatches = schedule.days.has(date.getDate());
|
|
317
|
+
const weekdayMatches = schedule.weekdays.has(date.getDay());
|
|
318
|
+
if (schedule.dayWildcard) return weekdayMatches;
|
|
319
|
+
if (schedule.weekdayWildcard) return dayMatches;
|
|
320
|
+
return dayMatches || weekdayMatches;
|
|
321
|
+
}
|
|
322
|
+
/** Reject impossible month/day pairs without spending the multi-year scan. */
|
|
323
|
+
function hasPossibleCalendarDay(schedule) {
|
|
324
|
+
if (schedule.dayWildcard || !schedule.weekdayWildcard) return true;
|
|
325
|
+
const maximumDay = /* @__PURE__ */ new Map([
|
|
326
|
+
[1, 31],
|
|
327
|
+
[2, 29],
|
|
328
|
+
[3, 31],
|
|
329
|
+
[4, 30],
|
|
330
|
+
[5, 31],
|
|
331
|
+
[6, 30],
|
|
332
|
+
[7, 31],
|
|
333
|
+
[8, 31],
|
|
334
|
+
[9, 30],
|
|
335
|
+
[10, 31],
|
|
336
|
+
[11, 30],
|
|
337
|
+
[12, 31]
|
|
338
|
+
]);
|
|
339
|
+
for (const month of schedule.months) {
|
|
340
|
+
const maximum = maximumDay.get(month) ?? 0;
|
|
341
|
+
if ([...schedule.days].some((day) => day <= maximum)) return true;
|
|
264
342
|
}
|
|
343
|
+
return false;
|
|
265
344
|
}
|
|
266
345
|
/** Parse one comma-list field into the match set. */
|
|
267
346
|
function parseField(field, min, max, out) {
|
|
@@ -298,11 +377,7 @@ window.__ModuleLoader__.load({
|
|
|
298
377
|
if (!schedule.minutes.has(date.getMinutes())) return false;
|
|
299
378
|
if (!schedule.hours.has(date.getHours())) return false;
|
|
300
379
|
if (!schedule.months.has(date.getMonth() + 1)) return false;
|
|
301
|
-
|
|
302
|
-
const weekdayMatches = schedule.weekdays.has(date.getDay());
|
|
303
|
-
if (schedule.dayWildcard) return weekdayMatches;
|
|
304
|
-
if (schedule.weekdayWildcard) return dayMatches;
|
|
305
|
-
return dayMatches || weekdayMatches;
|
|
380
|
+
return dayCandidate(schedule, date);
|
|
306
381
|
}
|
|
307
382
|
function isDigits(value) {
|
|
308
383
|
return /^\d+$/.test(value);
|
|
@@ -366,9 +441,9 @@ window.__ModuleLoader__.load({
|
|
|
366
441
|
* the core cron parser (schedule.ts) and the withSchedule transition.
|
|
367
442
|
*/
|
|
368
443
|
/**
|
|
369
|
-
* Set
|
|
370
|
-
* untouched); an enabled rule computes the
|
|
371
|
-
*
|
|
444
|
+
* Set an on-board task's schedule rule. A blank or invalid cron, or an
|
|
445
|
+
* archived task, is rejected (state untouched); an enabled rule computes the
|
|
446
|
+
* next run instant immediately, a disabled one carries no next-run instant.
|
|
372
447
|
* @param tasks - current ledger.
|
|
373
448
|
* @param id - the task to schedule.
|
|
374
449
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
@@ -376,7 +451,7 @@ window.__ModuleLoader__.load({
|
|
|
376
451
|
*/
|
|
377
452
|
function applySetSchedule(tasks, id, patch, now) {
|
|
378
453
|
const task = tasks.find((candidate) => candidate.id === id);
|
|
379
|
-
if (task === void 0) return {
|
|
454
|
+
if (task === void 0 || task.archivedAt !== void 0) return {
|
|
380
455
|
tasks,
|
|
381
456
|
applied: false
|
|
382
457
|
};
|
|
@@ -388,6 +463,10 @@ window.__ModuleLoader__.load({
|
|
|
388
463
|
};
|
|
389
464
|
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
390
465
|
const nextRunAt = enabled ? nextRunAtMs(cron, now) : void 0;
|
|
466
|
+
if (enabled && nextRunAt === void 0) return {
|
|
467
|
+
tasks,
|
|
468
|
+
applied: false
|
|
469
|
+
};
|
|
391
470
|
return {
|
|
392
471
|
tasks: tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
|
|
393
472
|
enabled,
|
|
@@ -408,7 +487,7 @@ window.__ModuleLoader__.load({
|
|
|
408
487
|
* @param now - clock instant (ms epoch).
|
|
409
488
|
*/
|
|
410
489
|
function applyScheduleNextRun(tasks, id, nextRunAt, lastTriggeredAt, now) {
|
|
411
|
-
return tasks.map((task) => task.id === id && task.schedule !== void 0 ? withSchedule(task, {
|
|
490
|
+
return tasks.map((task) => task.id === id && task.archivedAt === void 0 && task.schedule !== void 0 ? withSchedule(task, {
|
|
412
491
|
nextRunAt,
|
|
413
492
|
lastTriggeredAt
|
|
414
493
|
}, now) : task);
|
|
@@ -476,6 +555,9 @@ window.__ModuleLoader__.load({
|
|
|
476
555
|
function currentOf(sessions) {
|
|
477
556
|
return sessions.list.getSnapshot().current;
|
|
478
557
|
}
|
|
558
|
+
function messageOf(error) {
|
|
559
|
+
return error instanceof Error ? error.message : String(error);
|
|
560
|
+
}
|
|
479
561
|
/**
|
|
480
562
|
* Board controller (see module doc). All mutations bump the snapshot and
|
|
481
563
|
* persist through the store; UI and DOM mounts subscribe and re-render.
|
|
@@ -494,6 +576,12 @@ window.__ModuleLoader__.load({
|
|
|
494
576
|
disposers = [];
|
|
495
577
|
now;
|
|
496
578
|
uuid;
|
|
579
|
+
pendingTaskIds = /* @__PURE__ */ new Set();
|
|
580
|
+
taskQueues = /* @__PURE__ */ new Map();
|
|
581
|
+
transportError;
|
|
582
|
+
hostState;
|
|
583
|
+
remoteSubscribed = false;
|
|
584
|
+
remoteInitialization;
|
|
497
585
|
/** @param deps - store, execution service, and the sessions navigation face. */
|
|
498
586
|
constructor(deps) {
|
|
499
587
|
this.deps = deps;
|
|
@@ -503,11 +591,12 @@ window.__ModuleLoader__.load({
|
|
|
503
591
|
/** Load the persisted ledger and start the navigation/status subscriptions. */
|
|
504
592
|
start() {
|
|
505
593
|
this.tasks = this.deps.store.load();
|
|
506
|
-
this.
|
|
507
|
-
|
|
594
|
+
if (this.deps.transport !== void 0) this.initializeRemote();
|
|
595
|
+
else this.reconcileRunningTasks();
|
|
596
|
+
const unsubscribeExternal = this.deps.transport === void 0 ? this.deps.store.subscribeExternal?.(() => {
|
|
508
597
|
this.tasks = this.deps.store.load();
|
|
509
598
|
this.notify();
|
|
510
|
-
});
|
|
599
|
+
}) : void 0;
|
|
511
600
|
if (unsubscribeExternal !== void 0) this.disposers.push(unsubscribeExternal);
|
|
512
601
|
this.disposers.push(this.deps.sessions.list.subscribe(() => {
|
|
513
602
|
this.onSessionsChanged();
|
|
@@ -527,7 +616,10 @@ window.__ModuleLoader__.load({
|
|
|
527
616
|
boardOpen: this.boardOpen,
|
|
528
617
|
archiveView: this.archiveView,
|
|
529
618
|
selectedTaskId: this.selectedTaskId,
|
|
530
|
-
executionOptions: this.executionOptions
|
|
619
|
+
executionOptions: this.executionOptions,
|
|
620
|
+
pendingTaskIds: [...this.pendingTaskIds],
|
|
621
|
+
...this.transportError === void 0 ? {} : { transportError: this.transportError },
|
|
622
|
+
...this.hostState === void 0 ? {} : { host: this.hostState }
|
|
531
623
|
};
|
|
532
624
|
}
|
|
533
625
|
subscribe(fn) {
|
|
@@ -536,6 +628,14 @@ window.__ModuleLoader__.load({
|
|
|
536
628
|
this.listeners.delete(fn);
|
|
537
629
|
};
|
|
538
630
|
}
|
|
631
|
+
/** Whether production mutations are confirmed by the Host transport. */
|
|
632
|
+
isHostBacked() {
|
|
633
|
+
return this.deps.transport !== void 0;
|
|
634
|
+
}
|
|
635
|
+
/** Retry initial migration/state synchronization after an explicit Host error. */
|
|
636
|
+
async retryHostSync() {
|
|
637
|
+
return await this.initializeRemote();
|
|
638
|
+
}
|
|
539
639
|
openBoard() {
|
|
540
640
|
if (this.boardOpen) return;
|
|
541
641
|
this.lastCurrent = currentOf(this.deps.sessions);
|
|
@@ -575,13 +675,33 @@ window.__ModuleLoader__.load({
|
|
|
575
675
|
this.notify();
|
|
576
676
|
}
|
|
577
677
|
createTask(input) {
|
|
578
|
-
const
|
|
678
|
+
const id = this.uuid();
|
|
679
|
+
const { task, tasks } = applyCreateTask(this.tasks, input, this.now(), id);
|
|
579
680
|
if (task === void 0) return void 0;
|
|
580
681
|
this.tasks = [...tasks];
|
|
581
682
|
this.persistAndNotify();
|
|
582
683
|
return task;
|
|
583
684
|
}
|
|
685
|
+
/** Create through the Host and expose the task only after confirmation. */
|
|
686
|
+
async createTaskConfirmed(input) {
|
|
687
|
+
if (this.deps.transport === void 0) return this.createTask(input);
|
|
688
|
+
const id = this.uuid();
|
|
689
|
+
if (applyCreateTask(this.tasks, input, this.now(), id).task === void 0) return void 0;
|
|
690
|
+
return await this.commitRemote({
|
|
691
|
+
kind: "create",
|
|
692
|
+
id,
|
|
693
|
+
input
|
|
694
|
+
}, id) ? this.tasks.find((task) => task.id === id) : void 0;
|
|
695
|
+
}
|
|
584
696
|
updateTask(id, patch) {
|
|
697
|
+
if (this.deps.transport !== void 0) {
|
|
698
|
+
this.commitRemote({
|
|
699
|
+
kind: "update",
|
|
700
|
+
taskId: id,
|
|
701
|
+
patch
|
|
702
|
+
}, id);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
585
705
|
this.tasks = [...applyUpdateTask(this.tasks, id, patch, this.now())];
|
|
586
706
|
this.persistAndNotify();
|
|
587
707
|
}
|
|
@@ -597,10 +717,25 @@ window.__ModuleLoader__.load({
|
|
|
597
717
|
this.notify();
|
|
598
718
|
}
|
|
599
719
|
moveTask(id, status) {
|
|
720
|
+
if (this.deps.transport !== void 0) {
|
|
721
|
+
this.commitRemote({
|
|
722
|
+
kind: "move",
|
|
723
|
+
taskId: id,
|
|
724
|
+
status
|
|
725
|
+
}, id);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
600
728
|
this.tasks = this.tasks.map((task) => task.id === id ? withStatus(task, status, this.now()) : task);
|
|
601
729
|
this.persistAndNotify();
|
|
602
730
|
}
|
|
603
731
|
deleteTask(id) {
|
|
732
|
+
if (this.deps.transport !== void 0) {
|
|
733
|
+
this.commitRemote({
|
|
734
|
+
kind: "delete",
|
|
735
|
+
taskId: id
|
|
736
|
+
}, id);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
604
739
|
const { tasks, selectionCleared } = applyDeleteTask(this.tasks, this.selectedTaskId, id);
|
|
605
740
|
this.tasks = [...tasks];
|
|
606
741
|
if (selectionCleared) this.selectedTaskId = void 0;
|
|
@@ -615,6 +750,13 @@ window.__ModuleLoader__.load({
|
|
|
615
750
|
archiveTask(id) {
|
|
616
751
|
const { tasks, archived } = applyArchiveTask(this.tasks, id, this.now());
|
|
617
752
|
if (!archived) return false;
|
|
753
|
+
if (this.deps.transport !== void 0) {
|
|
754
|
+
this.commitRemote({
|
|
755
|
+
kind: "archive",
|
|
756
|
+
taskId: id
|
|
757
|
+
}, id);
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
618
760
|
this.tasks = [...tasks];
|
|
619
761
|
this.persistAndNotify();
|
|
620
762
|
return true;
|
|
@@ -623,7 +765,17 @@ window.__ModuleLoader__.load({
|
|
|
623
765
|
restoreTask(id) {
|
|
624
766
|
const { tasks, archived } = applyRestoreTask(this.tasks, id, this.now());
|
|
625
767
|
if (!archived) return false;
|
|
768
|
+
if (this.deps.transport !== void 0) {
|
|
769
|
+
this.commitRemote({
|
|
770
|
+
kind: "restore",
|
|
771
|
+
taskId: id
|
|
772
|
+
}, id).then((restored) => {
|
|
773
|
+
if (restored && this.selectedTaskId === id) this.closeTask();
|
|
774
|
+
});
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
626
777
|
this.tasks = [...tasks];
|
|
778
|
+
if (this.selectedTaskId === id) this.selectedTaskId = void 0;
|
|
627
779
|
this.persistAndNotify();
|
|
628
780
|
return true;
|
|
629
781
|
}
|
|
@@ -639,14 +791,21 @@ window.__ModuleLoader__.load({
|
|
|
639
791
|
setSchedule(id, patch) {
|
|
640
792
|
const { tasks, applied } = applySetSchedule(this.tasks, id, patch, this.now());
|
|
641
793
|
if (!applied) return false;
|
|
794
|
+
if (this.deps.transport !== void 0) {
|
|
795
|
+
this.commitRemote({
|
|
796
|
+
kind: "set-schedule",
|
|
797
|
+
taskId: id,
|
|
798
|
+
patch
|
|
799
|
+
}, id);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
642
802
|
this.tasks = [...tasks];
|
|
643
803
|
this.persistAndNotify();
|
|
644
804
|
return true;
|
|
645
805
|
}
|
|
646
806
|
/**
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
* schedule rule (it was deleted mid-tick, for example).
|
|
807
|
+
* Legacy pure-controller seam retained for migration-focused tests. The
|
|
808
|
+
* production browser never rolls schedules; the Host ledger owns them.
|
|
650
809
|
*/
|
|
651
810
|
applyScheduleNextRun(id, nextRunAt, lastTriggeredAt) {
|
|
652
811
|
const next = applyScheduleNextRun(this.tasks, id, nextRunAt, lastTriggeredAt, this.now());
|
|
@@ -654,12 +813,8 @@ window.__ModuleLoader__.load({
|
|
|
654
813
|
this.persistAndNotify();
|
|
655
814
|
}
|
|
656
815
|
/**
|
|
657
|
-
* Reload the
|
|
658
|
-
*
|
|
659
|
-
* tab (or a stale in-memory copy) can never be fired from this tab: the
|
|
660
|
-
* fire decision and the subsequent roll-forward both run on the freshest
|
|
661
|
-
* persisted truth. Deliberately silent — same-origin external changes still
|
|
662
|
-
* reach subscribers through the storage-event subscription.
|
|
816
|
+
* Reload the legacy v1 store without notifying subscribers. Production v2
|
|
817
|
+
* reads Host snapshots instead; this remains only for isolated legacy tests.
|
|
663
818
|
*/
|
|
664
819
|
reloadFromStore() {
|
|
665
820
|
this.tasks = this.deps.store.load();
|
|
@@ -679,11 +834,16 @@ window.__ModuleLoader__.load({
|
|
|
679
834
|
*/
|
|
680
835
|
async runTask(id) {
|
|
681
836
|
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
682
|
-
if (task === void 0 || task.status === "running") return false;
|
|
837
|
+
if (task === void 0 || task.archivedAt !== void 0 || task.status === "running") return false;
|
|
838
|
+
if (this.deps.transport !== void 0) return await this.commitRemote({
|
|
839
|
+
kind: "run",
|
|
840
|
+
taskId: id
|
|
841
|
+
}, id);
|
|
683
842
|
const { task: next, execution } = startExecution(task, this.now(), this.uuid());
|
|
684
843
|
this.tasks = this.tasks.map((candidate) => candidate.id === id ? next : candidate);
|
|
685
844
|
this.persistAndNotify();
|
|
686
845
|
this.activeExecutionIds.add(execution.id);
|
|
846
|
+
if (this.deps.exec === void 0) throw new Error("legacy execution service is unavailable");
|
|
687
847
|
await this.deps.exec.run(next, execution, (event) => {
|
|
688
848
|
this.handleExecutionEvent(event);
|
|
689
849
|
});
|
|
@@ -692,7 +852,14 @@ window.__ModuleLoader__.load({
|
|
|
692
852
|
/** Re-run a settled task: move it back to 'todo' first, then execute. */
|
|
693
853
|
async rerunTask(id) {
|
|
694
854
|
const task = this.tasks.find((candidate) => candidate.id === id);
|
|
695
|
-
if (task === void 0) return;
|
|
855
|
+
if (task === void 0 || task.archivedAt !== void 0) return;
|
|
856
|
+
if (this.deps.transport !== void 0) {
|
|
857
|
+
await this.commitRemote({
|
|
858
|
+
kind: "rerun",
|
|
859
|
+
taskId: id
|
|
860
|
+
}, id);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
696
863
|
if (task.status !== "running") {
|
|
697
864
|
this.tasks = this.tasks.map((candidate) => candidate.id === id ? withStatus(candidate, "todo", this.now()) : candidate);
|
|
698
865
|
this.persistAndNotify();
|
|
@@ -711,7 +878,7 @@ window.__ModuleLoader__.load({
|
|
|
711
878
|
}
|
|
712
879
|
/** Reconcile running tasks and close the board when the user navigates. */
|
|
713
880
|
onSessionsChanged() {
|
|
714
|
-
this.scheduleReconcile();
|
|
881
|
+
if (this.deps.transport === void 0) this.scheduleReconcile();
|
|
715
882
|
if (!this.boardOpen) return;
|
|
716
883
|
const current = currentOf(this.deps.sessions);
|
|
717
884
|
if (current !== this.lastCurrent) this.closeBoard();
|
|
@@ -739,6 +906,7 @@ window.__ModuleLoader__.load({
|
|
|
739
906
|
}
|
|
740
907
|
/** Settle tasks left 'running' whose sessions already finished. */
|
|
741
908
|
async reconcileRunningTasks() {
|
|
909
|
+
if (this.deps.exec === void 0) return;
|
|
742
910
|
if (this.reconcileInFlight) return;
|
|
743
911
|
this.reconcileInFlight = true;
|
|
744
912
|
try {
|
|
@@ -769,442 +937,138 @@ window.__ModuleLoader__.load({
|
|
|
769
937
|
}
|
|
770
938
|
}
|
|
771
939
|
persistAndNotify() {
|
|
772
|
-
this.deps.store.save(this.tasks);
|
|
940
|
+
if (this.deps.transport === void 0) this.deps.store.save(this.tasks);
|
|
773
941
|
this.notify();
|
|
774
942
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
updatedAt: now,
|
|
784
|
-
executions: task.executions.map((execution) => execution.id === executionId ? {
|
|
785
|
-
...execution,
|
|
786
|
-
sessionId
|
|
787
|
-
} : execution)
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
//#endregion
|
|
791
|
-
//#region src/core/execution.ts
|
|
792
|
-
/** Human copy for a run failure. */
|
|
793
|
-
function messageOf(error) {
|
|
794
|
-
if (error instanceof Error) return error.message;
|
|
795
|
-
return String(error);
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Whether a rejected preset switch actually means "the session already runs
|
|
799
|
-
* this preset" (the host's agent-preset-conflict with a matching
|
|
800
|
-
* existingPreset). A blank-session reuse race can produce this even though
|
|
801
|
-
* the requested composition is in place.
|
|
802
|
-
*/
|
|
803
|
-
function presetAlreadyRuns(error, mode) {
|
|
804
|
-
if (typeof error !== "object" || error === null) return false;
|
|
805
|
-
const details = error.details;
|
|
806
|
-
if (typeof details !== "object" || details === null) return false;
|
|
807
|
-
return details.existingPreset === mode;
|
|
808
|
-
}
|
|
809
|
-
/** Whether a `turn/end` payload closed the turn with an error reason. */
|
|
810
|
-
function isErrorTurnEnd(data) {
|
|
811
|
-
if (typeof data !== "object" || data === null) return false;
|
|
812
|
-
const reason = data.reason;
|
|
813
|
-
return typeof reason === "object" && reason !== null && reason.kind === "error";
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Run one task to completion (or to a settled failure).
|
|
817
|
-
*
|
|
818
|
-
* @param task - the task being executed.
|
|
819
|
-
* @param execution - the freshly opened execution record (id + start time).
|
|
820
|
-
* @param onEvent - callback for started/settled events.
|
|
821
|
-
* @returns resolves when the run settles (or fails to start); never rejects —
|
|
822
|
-
* every failure path is reported as a settled event.
|
|
823
|
-
*/
|
|
824
|
-
var ExecutionService = class {
|
|
825
|
-
env;
|
|
826
|
-
/**
|
|
827
|
-
* In-flight preset switches keyed by `sessionId\u0000mode`. The session
|
|
828
|
-
* list mirror can lag behind a just-applied switch, so concurrent runs
|
|
829
|
-
* against one blank session would otherwise each issue the same select
|
|
830
|
-
* RPC (an amplified storm); sharing one in-flight call keeps the wire to
|
|
831
|
-
* a single request. The settled entry is forgotten so a later run can
|
|
832
|
-
* switch again.
|
|
833
|
-
*/
|
|
834
|
-
presetSwitches = /* @__PURE__ */ new Map();
|
|
835
|
-
/** @param env - the runtime faces (real or fake). */
|
|
836
|
-
constructor(env) {
|
|
837
|
-
this.env = env;
|
|
838
|
-
}
|
|
839
|
-
async run(task, execution, onEvent) {
|
|
840
|
-
const settleFailed = (error) => {
|
|
841
|
-
onEvent({
|
|
842
|
-
kind: "settled",
|
|
843
|
-
taskId: task.id,
|
|
844
|
-
executionId: execution.id,
|
|
845
|
-
outcome: "failed",
|
|
846
|
-
error
|
|
847
|
-
});
|
|
848
|
-
};
|
|
943
|
+
async commitRemote(action, taskId) {
|
|
944
|
+
if (this.deps.transport === void 0) return true;
|
|
945
|
+
if (taskId === void 0) return await this.performRemote(action);
|
|
946
|
+
const operation = (this.taskQueues.get(taskId) ?? Promise.resolve()).catch(() => {}).then(async () => await this.performRemote(action));
|
|
947
|
+
const tail = operation.then(() => {}, () => {});
|
|
948
|
+
this.taskQueues.set(taskId, tail);
|
|
949
|
+
this.pendingTaskIds.add(taskId);
|
|
950
|
+
this.notify();
|
|
849
951
|
try {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
taskId
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
});
|
|
857
|
-
const driver = this.driverOf(sessionId);
|
|
858
|
-
if (driver === void 0) {
|
|
859
|
-
settleFailed("execution session is not ready");
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
if (!await this.applyMode(driver, task, sessionId, settleFailed)) return;
|
|
863
|
-
if (!await this.applyPermission(driver, task, settleFailed)) return;
|
|
864
|
-
await driver.rename(task.title).catch(() => {});
|
|
865
|
-
const baseline = driver.getSnapshot().turnEnds.size;
|
|
866
|
-
const accepted = await this.sendPrompt(driver, task);
|
|
867
|
-
if (!accepted.ok) {
|
|
868
|
-
settleFailed(messageOf(accepted.error));
|
|
869
|
-
return;
|
|
952
|
+
return await operation;
|
|
953
|
+
} finally {
|
|
954
|
+
if (this.taskQueues.get(taskId) === tail) {
|
|
955
|
+
this.taskQueues.delete(taskId);
|
|
956
|
+
this.pendingTaskIds.delete(taskId);
|
|
957
|
+
this.notify();
|
|
870
958
|
}
|
|
871
|
-
this.watchForSettlement(driver, task.id, execution.id, onEvent, baseline);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
settleFailed(messageOf(error));
|
|
874
959
|
}
|
|
875
960
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
*/
|
|
882
|
-
async applyMode(driver, task, sessionId, settleFailed) {
|
|
883
|
-
const mode = task.mode;
|
|
884
|
-
if (mode === void 0 || mode === "") return true;
|
|
885
|
-
const summary = this.env.sessions.list.getSnapshot().byId[sessionId];
|
|
886
|
-
if (summary?.blank === false) {
|
|
887
|
-
settleFailed(`cannot switch agent preset to ${mode}: the execution session is not blank`);
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
|
-
if (summary?.agentPreset === mode) return true;
|
|
891
|
-
const presets = this.env.presets;
|
|
892
|
-
if (presets === void 0) {
|
|
893
|
-
settleFailed(`this deployment does not support agent presets (task asks for ${mode})`);
|
|
894
|
-
return false;
|
|
895
|
-
}
|
|
961
|
+
async performRemote(action) {
|
|
962
|
+
const transport = this.deps.transport;
|
|
963
|
+
if (transport === void 0) return true;
|
|
964
|
+
this.transportError = void 0;
|
|
965
|
+
this.notify();
|
|
896
966
|
try {
|
|
897
|
-
|
|
898
|
-
if (!result.ok) {
|
|
899
|
-
if (presetAlreadyRuns(result.error, mode)) {
|
|
900
|
-
this.env.sessions.noteAgentPreset?.(sessionId, mode);
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
settleFailed(`agent preset switch to ${mode} rejected: ${messageOf(result.error)}`);
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
967
|
+
return this.acceptRemote(await transport.action(action)) || await this.refreshRemote();
|
|
906
968
|
} catch (error) {
|
|
907
|
-
|
|
969
|
+
await this.refreshRemote(messageOf(error));
|
|
908
970
|
return false;
|
|
909
971
|
}
|
|
910
|
-
this.env.sessions.noteAgentPreset?.(sessionId, mode);
|
|
911
|
-
return true;
|
|
912
972
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (inflight !== void 0) return inflight;
|
|
923
|
-
const attempt = presets.select(sessionId, mode).finally(() => {
|
|
924
|
-
if (this.presetSwitches.get(key) === attempt) this.presetSwitches.delete(key);
|
|
925
|
-
});
|
|
926
|
-
this.presetSwitches.set(key, attempt);
|
|
927
|
-
return attempt;
|
|
973
|
+
async initializeRemote() {
|
|
974
|
+
if (this.remoteInitialization !== void 0) return await this.remoteInitialization;
|
|
975
|
+
const initialization = this.doInitializeRemote();
|
|
976
|
+
this.remoteInitialization = initialization;
|
|
977
|
+
try {
|
|
978
|
+
return await initialization;
|
|
979
|
+
} finally {
|
|
980
|
+
if (this.remoteInitialization === initialization) this.remoteInitialization = void 0;
|
|
981
|
+
}
|
|
928
982
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
* admission is rejected or no command claimed the line.
|
|
933
|
-
*/
|
|
934
|
-
async applyPermission(driver, task, settleFailed) {
|
|
935
|
-
const permission = task.permission;
|
|
936
|
-
if (permission === void 0) return true;
|
|
937
|
-
const line = `/permission ${permission}`;
|
|
983
|
+
async doInitializeRemote() {
|
|
984
|
+
const transport = this.deps.transport;
|
|
985
|
+
if (transport === void 0) return true;
|
|
938
986
|
try {
|
|
939
|
-
|
|
940
|
-
if (!
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
settleFailed(`permission command not recognized: ${line}`);
|
|
946
|
-
return false;
|
|
987
|
+
this.acceptRemote(await transport.bootstrap(this.tasks));
|
|
988
|
+
if (!this.remoteSubscribed) {
|
|
989
|
+
this.remoteSubscribed = true;
|
|
990
|
+
this.disposers.push(transport.subscribe((event) => {
|
|
991
|
+
this.onRemoteEvent(event);
|
|
992
|
+
}));
|
|
947
993
|
}
|
|
994
|
+
return true;
|
|
948
995
|
} catch (error) {
|
|
949
|
-
|
|
996
|
+
this.transportError = messageOf(error);
|
|
997
|
+
this.notify();
|
|
950
998
|
return false;
|
|
951
999
|
}
|
|
952
|
-
return true;
|
|
953
1000
|
}
|
|
954
1001
|
/**
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
* runtime only maintains the window for the staged/current session), so the
|
|
960
|
-
* settled outcome is decided by the strongest available signal, in order:
|
|
961
|
-
* 1. the list summary — missing session → cancelled; still running → pending;
|
|
962
|
-
* 2. a warm conversation snapshot → `lastAgentError` decides failed/succeeded;
|
|
963
|
-
* 3. the raw history tail (when a history face is wired) — a `turn/end`
|
|
964
|
-
* error reason proves failure;
|
|
965
|
-
* 4. otherwise a finished session counts as succeeded.
|
|
966
|
-
*
|
|
967
|
-
* @param task - a task whose latest execution has no endedAt.
|
|
968
|
-
* @returns a settled event when the session state proves completion, else undefined.
|
|
1002
|
+
* SSE frames carry revision/scheduler/power. When the revision matches the
|
|
1003
|
+
* one already applied, apply the frame's scheduler/power in place and skip
|
|
1004
|
+
* the full /state fetch; otherwise the 5 s heartbeat would re-clone and
|
|
1005
|
+
* re-serialize the whole ledger per tab even while nothing changes.
|
|
969
1006
|
*/
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
executionId: execution.id,
|
|
980
|
-
outcome: "cancelled",
|
|
981
|
-
error: "execution session no longer exists"
|
|
982
|
-
};
|
|
983
|
-
if (summary.running) return void 0;
|
|
984
|
-
const driver = this.driverOf(execution.sessionId);
|
|
985
|
-
if (driver !== void 0) {
|
|
986
|
-
const snapshot = driver.getSnapshot();
|
|
987
|
-
if (snapshot.turnEnds.size > 0) {
|
|
988
|
-
const outcome = snapshot.lastAgentError !== null ? "failed" : "succeeded";
|
|
989
|
-
return {
|
|
990
|
-
kind: "settled",
|
|
991
|
-
taskId: task.id,
|
|
992
|
-
executionId: execution.id,
|
|
993
|
-
outcome,
|
|
994
|
-
error: snapshot.lastAgentError ?? void 0
|
|
995
|
-
};
|
|
996
|
-
}
|
|
1007
|
+
onRemoteEvent(event) {
|
|
1008
|
+
if (event !== void 0 && this.hostState !== void 0 && event.revision === this.hostState.revision && typeof event.scheduler === "object" && event.scheduler !== null && typeof event.power === "object" && event.power !== null) {
|
|
1009
|
+
this.hostState = {
|
|
1010
|
+
revision: event.revision,
|
|
1011
|
+
scheduler: event.scheduler,
|
|
1012
|
+
power: event.power
|
|
1013
|
+
};
|
|
1014
|
+
this.notify();
|
|
1015
|
+
return;
|
|
997
1016
|
}
|
|
998
|
-
|
|
999
|
-
kind: "settled",
|
|
1000
|
-
taskId: task.id,
|
|
1001
|
-
executionId: execution.id,
|
|
1002
|
-
outcome: "failed",
|
|
1003
|
-
error: "agent turn failed"
|
|
1004
|
-
};
|
|
1005
|
-
return {
|
|
1006
|
-
kind: "settled",
|
|
1007
|
-
taskId: task.id,
|
|
1008
|
-
executionId: execution.id,
|
|
1009
|
-
outcome: "succeeded"
|
|
1010
|
-
};
|
|
1017
|
+
this.refreshRemote();
|
|
1011
1018
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (history === void 0) return false;
|
|
1019
|
+
async refreshRemote(preserveError) {
|
|
1020
|
+
const transport = this.deps.transport;
|
|
1021
|
+
if (transport === void 0) return true;
|
|
1016
1022
|
try {
|
|
1017
|
-
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1023
|
+
this.acceptRemote(await transport.state());
|
|
1024
|
+
if (preserveError !== void 0) {
|
|
1025
|
+
this.transportError = preserveError;
|
|
1026
|
+
this.notify();
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1020
1029
|
} catch (error) {
|
|
1021
|
-
|
|
1030
|
+
this.transportError = preserveError ?? messageOf(error);
|
|
1031
|
+
this.notify();
|
|
1022
1032
|
return false;
|
|
1023
1033
|
}
|
|
1024
1034
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (workspaceId === void 0) throw new Error("no workspace available to run the task in");
|
|
1033
|
-
return this.env.workspaces.connectWorkspace(workspaceId);
|
|
1034
|
-
}
|
|
1035
|
-
driverOf(sessionId) {
|
|
1036
|
-
return this.env.sessions.binding(sessionId)?.session;
|
|
1037
|
-
}
|
|
1038
|
-
async sendPrompt(driver, task) {
|
|
1039
|
-
const text = task.prompt.trim() !== "" ? task.prompt : task.title;
|
|
1040
|
-
try {
|
|
1041
|
-
return await driver.prompt([{
|
|
1042
|
-
type: "text",
|
|
1043
|
-
text
|
|
1044
|
-
}], "queue");
|
|
1045
|
-
} catch (error) {
|
|
1046
|
-
return {
|
|
1047
|
-
ok: false,
|
|
1048
|
-
error
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Subscribe to the execution session and settle the run once the accepted
|
|
1054
|
-
* turn completes (turn counter advanced past the acceptance baseline and
|
|
1055
|
-
* the session is no longer running). Never settles while the session is
|
|
1056
|
-
* still running; unsubscribes on settle.
|
|
1057
|
-
*/
|
|
1058
|
-
watchForSettlement(driver, taskId, executionId, onEvent, baseline) {
|
|
1059
|
-
let settled = false;
|
|
1060
|
-
let unsubscribe = () => {};
|
|
1061
|
-
const check = () => {
|
|
1062
|
-
if (settled) return;
|
|
1063
|
-
const snapshot = driver.getSnapshot();
|
|
1064
|
-
if (snapshot.running || snapshot.turnEnds.size <= baseline) return;
|
|
1065
|
-
settled = true;
|
|
1066
|
-
unsubscribe();
|
|
1067
|
-
onEvent({
|
|
1068
|
-
kind: "settled",
|
|
1069
|
-
taskId,
|
|
1070
|
-
executionId,
|
|
1071
|
-
outcome: snapshot.lastAgentError !== null ? "failed" : "succeeded",
|
|
1072
|
-
error: snapshot.lastAgentError ?? void 0
|
|
1073
|
-
});
|
|
1035
|
+
acceptRemote(snapshot) {
|
|
1036
|
+
if (this.hostState?.scheduler.ledgerId === snapshot.scheduler.ledgerId && this.hostState !== void 0 && snapshot.revision < this.hostState.revision) return false;
|
|
1037
|
+
this.tasks = [...snapshot.tasks];
|
|
1038
|
+
this.hostState = {
|
|
1039
|
+
revision: snapshot.revision,
|
|
1040
|
+
scheduler: snapshot.scheduler,
|
|
1041
|
+
power: snapshot.power
|
|
1074
1042
|
};
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
//#region src/core/scheduler.ts
|
|
1081
|
-
/**
|
|
1082
|
-
* Browser-side scheduler: the heartbeat behind scheduled task runs.
|
|
1083
|
-
*
|
|
1084
|
-
* The board is a pure client plugin with no server channel, so "定时任务"
|
|
1085
|
-
* lives in the tab: a timer ticks every minute (plus immediately on tab
|
|
1086
|
-
* visibility recovery) and triggers any task whose `schedule.nextRunAt` is
|
|
1087
|
-
* due, rolling the schedule forward to the next cron match before triggering
|
|
1088
|
-
* so the same tick never double-fires. Missed runs are skipped, never queued:
|
|
1089
|
-
* a task still running at its due instant is skipped by the controller's
|
|
1090
|
-
* runTask guard and simply waits for the next cron match.
|
|
1091
|
-
*
|
|
1092
|
-
* A deleted task can never fire again from a stale in-memory copy: every
|
|
1093
|
-
* tick first re-reads the persisted ledger through the optional `refresh`
|
|
1094
|
-
* hook (the controller reloads its store), so the fire decision and the
|
|
1095
|
-
* subsequent roll-forward both run on the freshest persisted truth.
|
|
1096
|
-
*
|
|
1097
|
-
* The ticker is controlled: `start` arms a single interval guarded by an
|
|
1098
|
-
* idempotence check (a second start while running is a no-op, so wiring can
|
|
1099
|
-
* never leak a duplicate timer), and `stop`/`dispose` clear the timer and
|
|
1100
|
-
* drop the environment listeners. The board unmount path calls stop/dispose,
|
|
1101
|
-
* so no interval or listener outlives the board.
|
|
1102
|
-
*
|
|
1103
|
-
* Framework-free: all runtime access flows through the injected deps
|
|
1104
|
-
* (structural faces), so tests drive ticks directly without timers.
|
|
1105
|
-
*/
|
|
1106
|
-
/**
|
|
1107
|
-
* The schedule heartbeat (see module doc). `tick` is public so tests and
|
|
1108
|
-
* callers can drive a check without waiting for the interval.
|
|
1109
|
-
*/
|
|
1110
|
-
var SchedulerService = class {
|
|
1111
|
-
deps;
|
|
1112
|
-
timer;
|
|
1113
|
-
environmentListener;
|
|
1114
|
-
disposed = false;
|
|
1115
|
-
started = false;
|
|
1116
|
-
/** Cron expressions that matched no instant in the scan window, per task —
|
|
1117
|
-
* remembered so a never-matching rule is not re-scanned on every tick. */
|
|
1118
|
-
unmatchableRepair = /* @__PURE__ */ new Map();
|
|
1119
|
-
/** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
|
|
1120
|
-
constructor(deps) {
|
|
1121
|
-
this.deps = deps;
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Start ticking: one immediate check (catch-up after reload) + the interval.
|
|
1125
|
-
* Single-instance: a second start while already armed is a no-op, so a
|
|
1126
|
-
* re-entrant mount can never stack a duplicate timer.
|
|
1127
|
-
*/
|
|
1128
|
-
start() {
|
|
1129
|
-
if (this.disposed) return;
|
|
1130
|
-
if (this.started) return;
|
|
1131
|
-
this.started = true;
|
|
1132
|
-
this.tick();
|
|
1133
|
-
this.timer = setInterval(() => {
|
|
1134
|
-
this.tick();
|
|
1135
|
-
}, this.deps.tickMs ?? 6e4);
|
|
1136
|
-
if (this.deps.environment !== void 0) {
|
|
1137
|
-
this.environmentListener = () => {
|
|
1138
|
-
this.tick();
|
|
1139
|
-
};
|
|
1140
|
-
this.deps.environment.addEventListener("visibilitychange", this.environmentListener);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* Stop ticking and drop listeners (idempotent). Preferred shutdown verb for
|
|
1145
|
-
* callers that treat the scheduler as a controlled ticker; `dispose` is an
|
|
1146
|
-
* alias.
|
|
1147
|
-
*/
|
|
1148
|
-
stop() {
|
|
1149
|
-
this.dispose();
|
|
1150
|
-
}
|
|
1151
|
-
/** Stop ticking and drop listeners (idempotent). */
|
|
1152
|
-
dispose() {
|
|
1153
|
-
if (this.disposed && this.timer === void 0 && this.environmentListener === void 0) return;
|
|
1154
|
-
this.disposed = true;
|
|
1155
|
-
this.started = false;
|
|
1156
|
-
if (this.timer !== void 0) {
|
|
1157
|
-
clearInterval(this.timer);
|
|
1158
|
-
this.timer = void 0;
|
|
1159
|
-
}
|
|
1160
|
-
if (this.environmentListener !== void 0 && this.deps.environment !== void 0) {
|
|
1161
|
-
this.deps.environment.removeEventListener("visibilitychange", this.environmentListener);
|
|
1162
|
-
this.environmentListener = void 0;
|
|
1163
|
-
}
|
|
1043
|
+
this.transportError = void 0;
|
|
1044
|
+
if (this.selectedTaskId !== void 0 && !this.tasks.some((task) => task.id === this.selectedTaskId)) this.selectedTaskId = void 0;
|
|
1045
|
+
if (!this.archiveView && this.selectedTaskId !== void 0 && this.tasks.find((task) => task.id === this.selectedTaskId)?.archivedAt !== void 0) this.selectedTaskId = void 0;
|
|
1046
|
+
this.notify();
|
|
1047
|
+
return true;
|
|
1164
1048
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
* task per tick: the schedule is rolled forward only after runTask accepts
|
|
1168
|
-
* the run, so a rejected run keeps its due slot and is retried on the next
|
|
1169
|
-
* tick instead of being silently dropped.
|
|
1170
|
-
*/
|
|
1171
|
-
async tick() {
|
|
1172
|
-
if (this.disposed) return;
|
|
1173
|
-
if (this.deps.ready !== void 0 && !this.deps.ready()) return;
|
|
1174
|
-
this.deps.refresh?.();
|
|
1175
|
-
const now = this.deps.now();
|
|
1176
|
-
for (const task of this.deps.tasks()) {
|
|
1177
|
-
const schedule = task.schedule;
|
|
1178
|
-
if (schedule === void 0 || !schedule.enabled) continue;
|
|
1179
|
-
if (schedule.nextRunAt === void 0) {
|
|
1180
|
-
if (this.unmatchableRepair.get(task.id) === schedule.cron) continue;
|
|
1181
|
-
const repaired = nextRunAtMs(schedule.cron, now);
|
|
1182
|
-
if (repaired === void 0) {
|
|
1183
|
-
this.unmatchableRepair.set(task.id, schedule.cron);
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
this.unmatchableRepair.delete(task.id);
|
|
1187
|
-
this.deps.applySchedule(task.id, repaired, void 0);
|
|
1188
|
-
continue;
|
|
1189
|
-
}
|
|
1190
|
-
if (schedule.nextRunAt > now) continue;
|
|
1191
|
-
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
|
|
1192
|
-
if (await this.deps.runTask(task.id)) this.deps.applySchedule(task.id, next, now);
|
|
1193
|
-
}
|
|
1049
|
+
notify() {
|
|
1050
|
+
for (const fn of [...this.listeners]) fn();
|
|
1194
1051
|
}
|
|
1195
1052
|
};
|
|
1053
|
+
/** Record which session ran an execution (once the execution service reports it). */
|
|
1054
|
+
function attachSessionId(task, executionId, sessionId, now) {
|
|
1055
|
+
return {
|
|
1056
|
+
...task,
|
|
1057
|
+
updatedAt: now,
|
|
1058
|
+
executions: task.executions.map((execution) => execution.id === executionId ? {
|
|
1059
|
+
...execution,
|
|
1060
|
+
sessionId
|
|
1061
|
+
} : execution)
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1196
1064
|
//#endregion
|
|
1197
1065
|
//#region src/core/store.ts
|
|
1198
1066
|
/**
|
|
1199
|
-
*
|
|
1067
|
+
* Legacy v1 browser persistence and the store seam used by pure client tests.
|
|
1200
1068
|
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
* localStorage under a versioned key — the same persistence mechanism dsh's
|
|
1205
|
-
* own client snapshot stores use (`createSnapshotStore` persist). Data
|
|
1206
|
-
* survives page refreshes and dsh restarts (same origin), and survives
|
|
1207
|
-
* plugin uninstall (the key is simply left in place).
|
|
1069
|
+
* Production v2 state is Host-authoritative. This backend is retained only to
|
|
1070
|
+
* read `dsh.taskBoard.v1` for one-time import; the old value is never removed,
|
|
1071
|
+
* so it remains a read-only rollback copy after migration.
|
|
1208
1072
|
*
|
|
1209
1073
|
* The seam keeps the backend swappable (e.g. an IndexedDB or a host-file
|
|
1210
1074
|
* channel later); tests run against the in-memory backend and a jsdom
|
|
@@ -1402,8 +1266,12 @@ window.__ModuleLoader__.load({
|
|
|
1402
1266
|
"board.status.done": "已完成",
|
|
1403
1267
|
"board.status.failed": "已失败",
|
|
1404
1268
|
"board.runs": "次执行",
|
|
1269
|
+
"board.pending": "正在提交",
|
|
1405
1270
|
"board.updated": "更新于",
|
|
1406
1271
|
"board.created": "创建于",
|
|
1272
|
+
"board.hostError": "Host 操作失败:{error}",
|
|
1273
|
+
"board.retryHost": "重试连接 Host",
|
|
1274
|
+
"board.hostMeta": "Host 时区 {timeZone} · revision {revision}",
|
|
1407
1275
|
"new.title": "标题",
|
|
1408
1276
|
"new.titlePlaceholder": "一句话描述要做什么",
|
|
1409
1277
|
"new.description": "描述",
|
|
@@ -1472,11 +1340,17 @@ window.__ModuleLoader__.load({
|
|
|
1472
1340
|
"detail.executionSettings": "执行设置",
|
|
1473
1341
|
"exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。",
|
|
1474
1342
|
"settings.title": "任务看板",
|
|
1475
|
-
"settings.description": "
|
|
1343
|
+
"settings.description": "控制 Host 任务看板、agent 播报与运行期间的系统空闲睡眠保护。",
|
|
1476
1344
|
"settings.enabled": "启用任务看板",
|
|
1477
1345
|
"settings.enabledHint": "关闭后隐藏侧边栏入口与看板视图。",
|
|
1478
1346
|
"settings.announceToAgent": "向 agent 播报任务看板",
|
|
1479
1347
|
"settings.announceToAgentHint": "开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报,agent 仅在用户主动提及时了解看板。",
|
|
1348
|
+
"settings.preventIdleSleep": "阻止系统空闲睡眠",
|
|
1349
|
+
"settings.preventIdleSleepHint": "默认关闭。开启后,只要存在运行中的 DSH 会话、已启用的定时任务或会话状态尚未确认,Host 就阻止整机因空闲睡眠;屏幕仍可自动关闭。",
|
|
1350
|
+
"settings.powerStatus": "平台:{platform};保护状态:{phase};运行会话:{running};已启用计划:{schedules}",
|
|
1351
|
+
"settings.powerBoundary": "这可能增加电池消耗。合盖、手动睡眠、休眠、关机、低电量或企业策略不在保证范围内,也不会唤醒已经睡眠的机器。",
|
|
1352
|
+
"settings.powerUnknown": "未知",
|
|
1353
|
+
"settings.powerError": "最近一次电源保护错误:{error}",
|
|
1480
1354
|
"settings.inherit": "继承",
|
|
1481
1355
|
"settings.on": "开",
|
|
1482
1356
|
"settings.off": "关",
|
|
@@ -1513,8 +1387,12 @@ window.__ModuleLoader__.load({
|
|
|
1513
1387
|
"board.status.done": "Done",
|
|
1514
1388
|
"board.status.failed": "Failed",
|
|
1515
1389
|
"board.runs": "runs",
|
|
1390
|
+
"board.pending": "Submitting",
|
|
1516
1391
|
"board.updated": "Updated",
|
|
1517
1392
|
"board.created": "Created",
|
|
1393
|
+
"board.hostError": "Host action failed: {error}",
|
|
1394
|
+
"board.retryHost": "Retry Host connection",
|
|
1395
|
+
"board.hostMeta": "Host time zone {timeZone} · revision {revision}",
|
|
1518
1396
|
"new.title": "Title",
|
|
1519
1397
|
"new.titlePlaceholder": "What should be done, in one line",
|
|
1520
1398
|
"new.description": "Description",
|
|
@@ -1583,11 +1461,17 @@ window.__ModuleLoader__.load({
|
|
|
1583
1461
|
"detail.executionSettings": "Execution Settings",
|
|
1584
1462
|
"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
1463
|
"settings.title": "Task Board",
|
|
1586
|
-
"settings.description": "
|
|
1464
|
+
"settings.description": "Configure the Host task board, agent announcement, and idle-system-sleep protection while work is pending.",
|
|
1587
1465
|
"settings.enabled": "Enable the task board",
|
|
1588
1466
|
"settings.enabledHint": "When off, the sidebar entry and board view are hidden.",
|
|
1589
1467
|
"settings.announceToAgent": "Announce the task board to agents",
|
|
1590
1468
|
"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.",
|
|
1469
|
+
"settings.preventIdleSleep": "Prevent idle system sleep",
|
|
1470
|
+
"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.",
|
|
1471
|
+
"settings.powerStatus": "Platform: {platform}; protection: {phase}; running sessions: {running}; enabled schedules: {schedules}",
|
|
1472
|
+
"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.",
|
|
1473
|
+
"settings.powerUnknown": "unknown",
|
|
1474
|
+
"settings.powerError": "Latest power-protection error: {error}",
|
|
1591
1475
|
"settings.inherit": "Inherit",
|
|
1592
1476
|
"settings.on": "On",
|
|
1593
1477
|
"settings.off": "Off",
|
|
@@ -1717,7 +1601,7 @@ window.__ModuleLoader__.load({
|
|
|
1717
1601
|
//#region src/client/board/NewTaskModal.tsx
|
|
1718
1602
|
/**
|
|
1719
1603
|
* New-task modal: title + description + the prompt that execution will send.
|
|
1720
|
-
* Creates through the
|
|
1604
|
+
* Creates through the Host and closes only after the Host confirms it.
|
|
1721
1605
|
*/
|
|
1722
1606
|
/** New-task form overlay. */
|
|
1723
1607
|
function NewTaskModal({ controller, onClose }) {
|
|
@@ -1731,9 +1615,10 @@ window.__ModuleLoader__.load({
|
|
|
1731
1615
|
const [scheduleCron, setScheduleCron] = (0, react.useState)("");
|
|
1732
1616
|
const [scheduleError, setScheduleError] = (0, react.useState)(void 0);
|
|
1733
1617
|
const [error, setError] = (0, react.useState)(void 0);
|
|
1618
|
+
const [pending, setPending] = (0, react.useState)(false);
|
|
1734
1619
|
const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
|
|
1735
1620
|
(0, react.useEffect)(() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)), [controller]);
|
|
1736
|
-
const submit = () => {
|
|
1621
|
+
const submit = async () => {
|
|
1737
1622
|
if (scheduleEnabled) {
|
|
1738
1623
|
const cron = scheduleCron.trim();
|
|
1739
1624
|
if (cron === "" || !isValidCron(cron)) {
|
|
@@ -1741,7 +1626,8 @@ window.__ModuleLoader__.load({
|
|
|
1741
1626
|
return;
|
|
1742
1627
|
}
|
|
1743
1628
|
}
|
|
1744
|
-
|
|
1629
|
+
setPending(true);
|
|
1630
|
+
if (await controller.createTaskConfirmed({
|
|
1745
1631
|
title,
|
|
1746
1632
|
description,
|
|
1747
1633
|
prompt,
|
|
@@ -1753,7 +1639,8 @@ window.__ModuleLoader__.load({
|
|
|
1753
1639
|
cron: scheduleCron.trim()
|
|
1754
1640
|
} : void 0
|
|
1755
1641
|
}) === void 0) {
|
|
1756
|
-
|
|
1642
|
+
setPending(false);
|
|
1643
|
+
setError(controller.getSnapshot().transportError ?? t("new.required"));
|
|
1757
1644
|
return;
|
|
1758
1645
|
}
|
|
1759
1646
|
onClose();
|
|
@@ -1964,6 +1851,7 @@ window.__ModuleLoader__.load({
|
|
|
1964
1851
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1965
1852
|
type: "submit",
|
|
1966
1853
|
className: board_module_css_default.primaryButton,
|
|
1854
|
+
disabled: pending,
|
|
1967
1855
|
children: t("new.submit")
|
|
1968
1856
|
})]
|
|
1969
1857
|
})
|
|
@@ -1993,22 +1881,37 @@ window.__ModuleLoader__.load({
|
|
|
1993
1881
|
* by the board, so the memo boundary is effective.
|
|
1994
1882
|
*/
|
|
1995
1883
|
/** Compact relative/absolute time label. */
|
|
1996
|
-
function
|
|
1884
|
+
function formatHostTimestamp(ms, timeZone) {
|
|
1885
|
+
try {
|
|
1886
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
1887
|
+
dateStyle: "medium",
|
|
1888
|
+
timeStyle: "medium",
|
|
1889
|
+
...timeZone === void 0 ? {} : { timeZone }
|
|
1890
|
+
}).format(new Date(ms));
|
|
1891
|
+
} catch {
|
|
1892
|
+
return new Date(ms).toISOString();
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function formatTime(ms, timeZone) {
|
|
1997
1896
|
const date = new Date(ms);
|
|
1998
1897
|
const minutes = Math.floor((Date.now() - ms) / 6e4);
|
|
1999
1898
|
if (minutes < 1) return t("time.justNow");
|
|
2000
1899
|
if (minutes < 60) return `${minutes}m`;
|
|
2001
1900
|
if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
|
|
1901
|
+
if (timeZone !== void 0) return formatHostTimestamp(ms, timeZone);
|
|
2002
1902
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
2003
1903
|
}
|
|
2004
1904
|
/** One card in a column. */
|
|
2005
|
-
function TaskCardInner({ task, onClick }) {
|
|
1905
|
+
function TaskCardInner({ task, pending, timeZone, onClick }) {
|
|
2006
1906
|
const latest = task.executions[task.executions.length - 1];
|
|
2007
1907
|
const runs = task.executions.length;
|
|
1908
|
+
const archived = task.archivedAt !== void 0;
|
|
2008
1909
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2009
1910
|
type: "button",
|
|
2010
1911
|
className: board_module_css_default.card,
|
|
2011
|
-
"data-status": task.status,
|
|
1912
|
+
"data-status": archived ? "archived" : task.status,
|
|
1913
|
+
"data-dsh-part": "card",
|
|
1914
|
+
"data-pending": pending || void 0,
|
|
2012
1915
|
onClick,
|
|
2013
1916
|
title: task.description !== "" ? task.description : task.title,
|
|
2014
1917
|
children: [
|
|
@@ -2031,14 +1934,14 @@ window.__ModuleLoader__.load({
|
|
|
2031
1934
|
formatTime(task.updatedAt)
|
|
2032
1935
|
]
|
|
2033
1936
|
}),
|
|
2034
|
-
task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1937
|
+
!archived && task.schedule?.enabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2035
1938
|
className: board_module_css_default.cardSchedule,
|
|
2036
|
-
title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${
|
|
1939
|
+
title: task.schedule.nextRunAt !== void 0 ? `${t("card.scheduled")} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}` : t("card.scheduled"),
|
|
2037
1940
|
children: t("card.scheduled")
|
|
2038
1941
|
}),
|
|
2039
1942
|
latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2040
1943
|
className: board_module_css_default.cardRun,
|
|
2041
|
-
"data-result": latest.result,
|
|
1944
|
+
"data-result": archived ? void 0 : latest.result,
|
|
2042
1945
|
children: [
|
|
2043
1946
|
runs,
|
|
2044
1947
|
" ",
|
|
@@ -2050,13 +1953,17 @@ window.__ModuleLoader__.load({
|
|
|
2050
1953
|
title: latest.sessionId,
|
|
2051
1954
|
children: "⌁"
|
|
2052
1955
|
}),
|
|
2053
|
-
task.status === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1956
|
+
!archived && (task.status === "running" || pending) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2054
1957
|
className: board_module_css_default.cardSpinner,
|
|
2055
1958
|
"aria-hidden": "true"
|
|
2056
1959
|
})
|
|
2057
1960
|
]
|
|
2058
1961
|
}),
|
|
2059
|
-
|
|
1962
|
+
!archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1963
|
+
className: board_module_css_default.cardRunningLabel,
|
|
1964
|
+
children: [t("board.pending"), "…"]
|
|
1965
|
+
}),
|
|
1966
|
+
!archived && latest !== void 0 && executionLabel(latest) === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2060
1967
|
className: board_module_css_default.cardRunningLabel,
|
|
2061
1968
|
children: [t("detail.result.running"), "…"]
|
|
2062
1969
|
})
|
|
@@ -2123,7 +2030,7 @@ window.__ModuleLoader__.load({
|
|
|
2123
2030
|
cancelled: "detail.result.cancelled"
|
|
2124
2031
|
};
|
|
2125
2032
|
/** One execution-history row. */
|
|
2126
|
-
function ExecutionRow({ execution, onOpen }) {
|
|
2033
|
+
function ExecutionRow({ execution, timeZone, onOpen }) {
|
|
2127
2034
|
const result = execution.result;
|
|
2128
2035
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2129
2036
|
className: board_module_css_default.executionRow,
|
|
@@ -2139,8 +2046,8 @@ window.__ModuleLoader__.load({
|
|
|
2139
2046
|
children: [
|
|
2140
2047
|
t("detail.executionStarted"),
|
|
2141
2048
|
" ",
|
|
2142
|
-
formatTime(execution.startedAt),
|
|
2143
|
-
execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt)}`
|
|
2049
|
+
formatTime(execution.startedAt, timeZone),
|
|
2050
|
+
execution.endedAt !== void 0 && ` · ${t("detail.executionEnded")} ${formatTime(execution.endedAt, timeZone)}`
|
|
2144
2051
|
]
|
|
2145
2052
|
}),
|
|
2146
2053
|
execution.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -2160,7 +2067,7 @@ window.__ModuleLoader__.load({
|
|
|
2160
2067
|
});
|
|
2161
2068
|
}
|
|
2162
2069
|
/** The execution-target editor: workspace / mode / permission pickers. */
|
|
2163
|
-
function ExecutionSettingsSection({ controller, task }) {
|
|
2070
|
+
function ExecutionSettingsSection({ controller, task, pending }) {
|
|
2164
2071
|
const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
|
|
2165
2072
|
(0, react.useEffect)(() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)), [controller]);
|
|
2166
2073
|
const workspaceId = task.workspaceId ?? "";
|
|
@@ -2184,6 +2091,7 @@ window.__ModuleLoader__.load({
|
|
|
2184
2091
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2185
2092
|
className: board_module_css_default.select,
|
|
2186
2093
|
value: workspaceId,
|
|
2094
|
+
disabled: pending,
|
|
2187
2095
|
onChange: (event) => {
|
|
2188
2096
|
controller.updateTask(task.id, { workspaceId: event.target.value });
|
|
2189
2097
|
},
|
|
@@ -2211,6 +2119,7 @@ window.__ModuleLoader__.load({
|
|
|
2211
2119
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2212
2120
|
className: board_module_css_default.select,
|
|
2213
2121
|
value: mode,
|
|
2122
|
+
disabled: pending,
|
|
2214
2123
|
onChange: (event) => {
|
|
2215
2124
|
controller.updateTask(task.id, { mode: event.target.value });
|
|
2216
2125
|
},
|
|
@@ -2243,6 +2152,7 @@ window.__ModuleLoader__.load({
|
|
|
2243
2152
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2244
2153
|
className: board_module_css_default.select,
|
|
2245
2154
|
value: permission,
|
|
2155
|
+
disabled: pending,
|
|
2246
2156
|
onChange: (event) => {
|
|
2247
2157
|
controller.updateTask(task.id, { permission: event.target.value === "" ? void 0 : event.target.value });
|
|
2248
2158
|
},
|
|
@@ -2259,13 +2169,14 @@ window.__ModuleLoader__.load({
|
|
|
2259
2169
|
});
|
|
2260
2170
|
}
|
|
2261
2171
|
/** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
|
|
2262
|
-
function ScheduleSection({ controller, task }) {
|
|
2172
|
+
function ScheduleSection({ controller, task, pending }) {
|
|
2263
2173
|
const schedule = task.schedule;
|
|
2264
2174
|
const [cron, setCron] = (0, react.useState)(schedule?.cron ?? "0 9 * * *");
|
|
2265
2175
|
const [enabled, setEnabled] = (0, react.useState)(schedule?.enabled ?? false);
|
|
2266
2176
|
const [nextRunAt, setNextRunAt] = (0, react.useState)(schedule?.nextRunAt);
|
|
2267
2177
|
const [lastTriggeredAt, setLastTriggeredAt] = (0, react.useState)(schedule?.lastTriggeredAt);
|
|
2268
2178
|
const [error, setError] = (0, react.useState)(void 0);
|
|
2179
|
+
const timeZone = controller.getSnapshot().host?.scheduler.timeZone;
|
|
2269
2180
|
(0, react.useEffect)(() => {
|
|
2270
2181
|
setCron(schedule?.cron ?? "0 9 * * *");
|
|
2271
2182
|
setEnabled(schedule?.enabled ?? false);
|
|
@@ -2298,8 +2209,10 @@ window.__ModuleLoader__.load({
|
|
|
2298
2209
|
return;
|
|
2299
2210
|
}
|
|
2300
2211
|
setError(void 0);
|
|
2301
|
-
if (
|
|
2302
|
-
|
|
2212
|
+
if (controller.setSchedule(task.id, {
|
|
2213
|
+
enabled: next,
|
|
2214
|
+
...next && trimmed !== schedule?.cron ? { cron: trimmed } : {}
|
|
2215
|
+
}) && !controller.isHostBacked()) setEnabled(next);
|
|
2303
2216
|
};
|
|
2304
2217
|
const applyPreset = (preset) => {
|
|
2305
2218
|
if (preset === "") return;
|
|
@@ -2307,8 +2220,8 @@ window.__ModuleLoader__.load({
|
|
|
2307
2220
|
setError(void 0);
|
|
2308
2221
|
controller.setSchedule(task.id, { cron: preset });
|
|
2309
2222
|
};
|
|
2310
|
-
const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= Date.now() ? t("detail.schedule.dueSoon") :
|
|
2311
|
-
const lastLabel = lastTriggeredAt === void 0 ? "—" :
|
|
2223
|
+
const nextLabel = !enabled || nextRunAt === void 0 ? t("detail.schedule.notScheduled") : nextRunAt <= Date.now() ? t("detail.schedule.dueSoon") : formatHostTimestamp(nextRunAt, timeZone);
|
|
2224
|
+
const lastLabel = lastTriggeredAt === void 0 ? "—" : formatHostTimestamp(lastTriggeredAt, timeZone);
|
|
2312
2225
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2313
2226
|
className: board_module_css_default.detailSection,
|
|
2314
2227
|
children: [
|
|
@@ -2318,6 +2231,7 @@ window.__ModuleLoader__.load({
|
|
|
2318
2231
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2319
2232
|
type: "checkbox",
|
|
2320
2233
|
checked: enabled,
|
|
2234
|
+
disabled: pending,
|
|
2321
2235
|
onChange: (event) => {
|
|
2322
2236
|
toggleEnabled(event.target.checked);
|
|
2323
2237
|
}
|
|
@@ -2328,6 +2242,7 @@ window.__ModuleLoader__.load({
|
|
|
2328
2242
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2329
2243
|
className: `${board_module_css_default.input} ${board_module_css_default.scheduleInput}${error !== void 0 ? ` ${board_module_css_default.scheduleInputInvalid}` : ""}`,
|
|
2330
2244
|
value: cron,
|
|
2245
|
+
disabled: pending,
|
|
2331
2246
|
placeholder: "0 9 * * *",
|
|
2332
2247
|
spellCheck: false,
|
|
2333
2248
|
"aria-label": t("detail.schedule.cron"),
|
|
@@ -2344,6 +2259,7 @@ window.__ModuleLoader__.load({
|
|
|
2344
2259
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2345
2260
|
className: board_module_css_default.schedulePreset,
|
|
2346
2261
|
value: "",
|
|
2262
|
+
disabled: pending,
|
|
2347
2263
|
"aria-label": t("detail.schedule.presets"),
|
|
2348
2264
|
onChange: (event) => {
|
|
2349
2265
|
applyPreset(event.target.value);
|
|
@@ -2379,12 +2295,17 @@ window.__ModuleLoader__.load({
|
|
|
2379
2295
|
/** Task detail overlay. */
|
|
2380
2296
|
function TaskDetail({ controller, task }) {
|
|
2381
2297
|
const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
|
|
2382
|
-
const running = task.status === "running";
|
|
2383
2298
|
const [latest, setLatest] = (0, react.useState)(task);
|
|
2384
2299
|
(0, react.useEffect)(() => {
|
|
2385
2300
|
setLatest(task);
|
|
2386
2301
|
}, [task]);
|
|
2387
2302
|
const current = latest;
|
|
2303
|
+
const snapshot = controller.getSnapshot();
|
|
2304
|
+
const running = current.status === "running";
|
|
2305
|
+
const archived = current.archivedAt !== void 0;
|
|
2306
|
+
const pending = snapshot.pendingTaskIds.includes(current.id);
|
|
2307
|
+
const transportError = snapshot.transportError;
|
|
2308
|
+
const timeZone = snapshot.host?.scheduler.timeZone;
|
|
2388
2309
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2389
2310
|
className: board_module_css_default.modalBackdrop,
|
|
2390
2311
|
onMouseDown: (event) => {
|
|
@@ -2404,8 +2325,8 @@ window.__ModuleLoader__.load({
|
|
|
2404
2325
|
}),
|
|
2405
2326
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2406
2327
|
className: board_module_css_default.statusBadge,
|
|
2407
|
-
"data-status": current.status,
|
|
2408
|
-
children: t(STATUS_KEY[current.status])
|
|
2328
|
+
"data-status": archived ? "archived" : current.status,
|
|
2329
|
+
children: archived ? t("board.archive") : t(STATUS_KEY[current.status])
|
|
2409
2330
|
}),
|
|
2410
2331
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2411
2332
|
type: "button",
|
|
@@ -2421,6 +2342,21 @@ window.__ModuleLoader__.load({
|
|
|
2421
2342
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2422
2343
|
className: board_module_css_default.detailBody,
|
|
2423
2344
|
children: [
|
|
2345
|
+
transportError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2346
|
+
className: board_module_css_default.formError,
|
|
2347
|
+
children: [
|
|
2348
|
+
t("board.hostError", { error: transportError }),
|
|
2349
|
+
" ",
|
|
2350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2351
|
+
type: "button",
|
|
2352
|
+
className: board_module_css_default.linkButton,
|
|
2353
|
+
onClick: () => {
|
|
2354
|
+
controller.retryHostSync();
|
|
2355
|
+
},
|
|
2356
|
+
children: t("board.retryHost")
|
|
2357
|
+
})
|
|
2358
|
+
]
|
|
2359
|
+
}),
|
|
2424
2360
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2425
2361
|
className: board_module_css_default.detailSection,
|
|
2426
2362
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.description") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -2435,14 +2371,15 @@ window.__ModuleLoader__.load({
|
|
|
2435
2371
|
children: current.prompt !== "" ? current.prompt : current.title
|
|
2436
2372
|
})]
|
|
2437
2373
|
}),
|
|
2438
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
|
|
2374
|
+
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
|
|
2439
2375
|
controller,
|
|
2440
|
-
task: current
|
|
2441
|
-
|
|
2442
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
|
|
2376
|
+
task: current,
|
|
2377
|
+
pending
|
|
2378
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
|
|
2443
2379
|
controller,
|
|
2444
|
-
task: current
|
|
2445
|
-
|
|
2380
|
+
task: current,
|
|
2381
|
+
pending
|
|
2382
|
+
})] }),
|
|
2446
2383
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2447
2384
|
className: board_module_css_default.detailSection,
|
|
2448
2385
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.execution") }), current.executions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -2452,20 +2389,21 @@ window.__ModuleLoader__.load({
|
|
|
2452
2389
|
className: board_module_css_default.executionList,
|
|
2453
2390
|
children: [...current.executions].reverse().map((execution) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionRow, {
|
|
2454
2391
|
execution,
|
|
2392
|
+
timeZone,
|
|
2455
2393
|
onOpen: (sessionId) => {
|
|
2456
2394
|
controller.openSession(sessionId);
|
|
2457
2395
|
}
|
|
2458
2396
|
}, execution.id))
|
|
2459
2397
|
})]
|
|
2460
2398
|
}),
|
|
2461
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2399
|
+
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2462
2400
|
className: board_module_css_default.detailSection,
|
|
2463
2401
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("board.status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2464
2402
|
className: board_module_css_default.moveRow,
|
|
2465
2403
|
children: MANUAL_STATUSES.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2466
2404
|
type: "button",
|
|
2467
2405
|
className: board_module_css_default.ghostButton,
|
|
2468
|
-
disabled: current.status === status || running,
|
|
2406
|
+
disabled: current.status === status || running || pending,
|
|
2469
2407
|
onClick: () => {
|
|
2470
2408
|
controller.moveTask(current.id, status);
|
|
2471
2409
|
},
|
|
@@ -2478,36 +2416,42 @@ window.__ModuleLoader__.load({
|
|
|
2478
2416
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
2479
2417
|
className: board_module_css_default.detailFooter,
|
|
2480
2418
|
children: [
|
|
2481
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2419
|
+
!archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2420
|
+
className: board_module_css_default.detailMeta,
|
|
2421
|
+
children: [t("board.pending"), "…"]
|
|
2422
|
+
}),
|
|
2423
|
+
!archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2482
2424
|
type: "button",
|
|
2483
2425
|
className: board_module_css_default.primaryButton,
|
|
2484
|
-
disabled: running,
|
|
2426
|
+
disabled: running || pending,
|
|
2485
2427
|
onClick: () => {
|
|
2486
|
-
controller.
|
|
2487
|
-
|
|
2428
|
+
controller.rerunTask(current.id).then(() => {
|
|
2429
|
+
if (controller.getSnapshot().transportError === void 0) controller.closeTask();
|
|
2430
|
+
});
|
|
2488
2431
|
},
|
|
2489
2432
|
children: current.executions.length === 0 ? t("detail.run") : t("detail.rerun")
|
|
2490
2433
|
}),
|
|
2491
|
-
|
|
2434
|
+
archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2492
2435
|
type: "button",
|
|
2493
2436
|
className: board_module_css_default.primaryButton,
|
|
2437
|
+
disabled: pending,
|
|
2494
2438
|
onClick: () => {
|
|
2495
2439
|
controller.restoreTask(current.id);
|
|
2496
|
-
controller.closeTask();
|
|
2497
2440
|
},
|
|
2498
2441
|
children: t("detail.restore")
|
|
2499
2442
|
}) : (current.status === "done" || current.status === "failed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2500
2443
|
type: "button",
|
|
2501
2444
|
className: board_module_css_default.ghostButton,
|
|
2445
|
+
disabled: pending,
|
|
2502
2446
|
onClick: () => {
|
|
2503
2447
|
controller.archiveTask(current.id);
|
|
2504
|
-
controller.closeTask();
|
|
2505
2448
|
},
|
|
2506
2449
|
children: t("detail.archive")
|
|
2507
2450
|
}),
|
|
2508
2451
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2509
2452
|
type: "button",
|
|
2510
2453
|
className: board_module_css_default.dangerButton,
|
|
2454
|
+
disabled: pending,
|
|
2511
2455
|
onClick: () => {
|
|
2512
2456
|
setConfirmDelete(true);
|
|
2513
2457
|
},
|
|
@@ -2518,8 +2462,8 @@ window.__ModuleLoader__.load({
|
|
|
2518
2462
|
children: [
|
|
2519
2463
|
t("board.created"),
|
|
2520
2464
|
" ",
|
|
2521
|
-
formatTime(current.createdAt),
|
|
2522
|
-
|
|
2465
|
+
formatTime(current.createdAt, timeZone),
|
|
2466
|
+
archived && ` · ${t("detail.archivedAt", { time: formatTime(current.archivedAt, timeZone) })}`
|
|
2523
2467
|
]
|
|
2524
2468
|
})
|
|
2525
2469
|
]
|
|
@@ -2536,7 +2480,6 @@ window.__ModuleLoader__.load({
|
|
|
2536
2480
|
onConfirm: () => {
|
|
2537
2481
|
setConfirmDelete(false);
|
|
2538
2482
|
controller.deleteTask(current.id);
|
|
2539
|
-
controller.closeTask();
|
|
2540
2483
|
}
|
|
2541
2484
|
})]
|
|
2542
2485
|
});
|
|
@@ -2560,9 +2503,11 @@ window.__ModuleLoader__.load({
|
|
|
2560
2503
|
* re-renders only when its own task changes — not when a sibling card status,
|
|
2561
2504
|
* the filter, or the selection moves.
|
|
2562
2505
|
*/
|
|
2563
|
-
const MemoTaskCard = (0, react.memo)(function MemoTaskCard({ task, onOpen }) {
|
|
2506
|
+
const MemoTaskCard = (0, react.memo)(function MemoTaskCard({ task, pending, timeZone, onOpen }) {
|
|
2564
2507
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
|
|
2565
2508
|
task,
|
|
2509
|
+
pending,
|
|
2510
|
+
timeZone,
|
|
2566
2511
|
onClick: (0, react.useCallback)(() => {
|
|
2567
2512
|
onOpen(task.id);
|
|
2568
2513
|
}, [task.id, onOpen])
|
|
@@ -2583,6 +2528,7 @@ window.__ModuleLoader__.load({
|
|
|
2583
2528
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2584
2529
|
className: board_module_css_default.board,
|
|
2585
2530
|
"data-dsh-taskboard-board": "",
|
|
2531
|
+
"data-dsh-plugin": "task-board",
|
|
2586
2532
|
children: [
|
|
2587
2533
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
2588
2534
|
className: board_module_css_default.boardHeader,
|
|
@@ -2603,6 +2549,13 @@ window.__ModuleLoader__.load({
|
|
|
2603
2549
|
className: board_module_css_default.boardTitle,
|
|
2604
2550
|
children: t("board.title")
|
|
2605
2551
|
}),
|
|
2552
|
+
snapshot.host !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2553
|
+
className: board_module_css_default.detailMeta,
|
|
2554
|
+
children: t("board.hostMeta", {
|
|
2555
|
+
revision: String(snapshot.host.revision),
|
|
2556
|
+
timeZone: snapshot.host.scheduler.timeZone
|
|
2557
|
+
})
|
|
2558
|
+
}),
|
|
2606
2559
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2607
2560
|
className: board_module_css_default.search,
|
|
2608
2561
|
type: "search",
|
|
@@ -2631,11 +2584,27 @@ window.__ModuleLoader__.load({
|
|
|
2631
2584
|
})
|
|
2632
2585
|
]
|
|
2633
2586
|
}),
|
|
2587
|
+
snapshot.transportError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2588
|
+
className: board_module_css_default.formError,
|
|
2589
|
+
children: [
|
|
2590
|
+
t("board.hostError", { error: snapshot.transportError }),
|
|
2591
|
+
" ",
|
|
2592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2593
|
+
type: "button",
|
|
2594
|
+
className: board_module_css_default.linkButton,
|
|
2595
|
+
onClick: () => {
|
|
2596
|
+
controller.retryHostSync();
|
|
2597
|
+
},
|
|
2598
|
+
children: t("board.retryHost")
|
|
2599
|
+
})
|
|
2600
|
+
]
|
|
2601
|
+
}),
|
|
2634
2602
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2635
2603
|
className: board_module_css_default.columns,
|
|
2636
2604
|
children: archiveView ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2637
2605
|
className: board_module_css_default.column,
|
|
2638
2606
|
"data-status": "archived",
|
|
2607
|
+
"data-dsh-part": "column",
|
|
2639
2608
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
2640
2609
|
className: board_module_css_default.columnHeader,
|
|
2641
2610
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
@@ -2649,6 +2618,8 @@ window.__ModuleLoader__.load({
|
|
|
2649
2618
|
className: board_module_css_default.cards,
|
|
2650
2619
|
children: [visible.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoTaskCard, {
|
|
2651
2620
|
task,
|
|
2621
|
+
pending: snapshot.pendingTaskIds.includes(task.id),
|
|
2622
|
+
timeZone: snapshot.host?.scheduler.timeZone,
|
|
2652
2623
|
onOpen: openTask
|
|
2653
2624
|
}, task.id)), visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2654
2625
|
className: board_module_css_default.columnEmpty,
|
|
@@ -2660,6 +2631,7 @@ window.__ModuleLoader__.load({
|
|
|
2660
2631
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2661
2632
|
className: board_module_css_default.column,
|
|
2662
2633
|
"data-status": column.status,
|
|
2634
|
+
"data-dsh-part": "column",
|
|
2663
2635
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
2664
2636
|
className: board_module_css_default.columnHeader,
|
|
2665
2637
|
children: [
|
|
@@ -2681,6 +2653,8 @@ window.__ModuleLoader__.load({
|
|
|
2681
2653
|
className: board_module_css_default.cards,
|
|
2682
2654
|
children: [tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoTaskCard, {
|
|
2683
2655
|
task,
|
|
2656
|
+
pending: snapshot.pendingTaskIds.includes(task.id),
|
|
2657
|
+
timeZone: snapshot.host?.scheduler.timeZone,
|
|
2684
2658
|
onOpen: openTask
|
|
2685
2659
|
}, task.id)), tasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2686
2660
|
className: board_module_css_default.columnEmpty,
|
|
@@ -2745,6 +2719,7 @@ window.__ModuleLoader__.load({
|
|
|
2745
2719
|
if (column === void 0) return;
|
|
2746
2720
|
container = document.createElement("div");
|
|
2747
2721
|
container.dataset.dshTaskboardView = "";
|
|
2722
|
+
container.dataset.dshPlugin = "task-board";
|
|
2748
2723
|
container.className = board_module_css_default.boardView;
|
|
2749
2724
|
column.appendChild(container);
|
|
2750
2725
|
root = (0, react_dom_client.createRoot)(container);
|
|
@@ -2792,9 +2767,7 @@ window.__ModuleLoader__.load({
|
|
|
2792
2767
|
};
|
|
2793
2768
|
}
|
|
2794
2769
|
//#endregion
|
|
2795
|
-
//#region src/client/sidebar-entry.ts
|
|
2796
|
-
/** Inline icon (matches the shell's 16px nav-icon look). */
|
|
2797
|
-
const ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>`;
|
|
2770
|
+
//#region src/client/sidebar-entry-core.ts
|
|
2798
2771
|
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
2799
2772
|
function sidebarRoot() {
|
|
2800
2773
|
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
|
|
@@ -2808,27 +2781,30 @@ window.__ModuleLoader__.load({
|
|
|
2808
2781
|
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
2809
2782
|
}
|
|
2810
2783
|
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
2811
|
-
function createEntry(
|
|
2784
|
+
function createEntry(options) {
|
|
2812
2785
|
const entry = document.createElement("button");
|
|
2813
2786
|
entry.type = "button";
|
|
2814
|
-
entry.
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2787
|
+
entry.setAttribute(options.rowAttribute, "");
|
|
2788
|
+
if (options.plugin !== void 0) {
|
|
2789
|
+
entry.setAttribute("data-dsh-plugin", options.plugin);
|
|
2790
|
+
entry.setAttribute("data-dsh-part", "sidebar-entry");
|
|
2791
|
+
}
|
|
2792
|
+
entry.className = options.css["entry"] ?? "";
|
|
2793
|
+
entry.setAttribute("aria-label", options.label());
|
|
2794
|
+
if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
|
|
2795
|
+
entry.innerHTML = "<span class=\"" + (options.css["entryIcon"] ?? "") + "\">" + options.icon + "</span><span class=\"" + (options.css["entryLabel"] ?? "") + "\">" + options.label() + "</span>";
|
|
2796
|
+
entry.addEventListener("click", options.onToggle);
|
|
2821
2797
|
return entry;
|
|
2822
2798
|
}
|
|
2823
2799
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
2824
|
-
function placeEntry(root, entry) {
|
|
2800
|
+
function placeEntry(root, entry, options) {
|
|
2825
2801
|
const button = newSessionButton(root);
|
|
2826
2802
|
if (button === void 0) return false;
|
|
2827
2803
|
if (entry.parentElement !== root) {
|
|
2828
2804
|
const row = button.closest("[class*=\"logoRow\"]");
|
|
2829
2805
|
const base = row !== null && row.parentElement === root ? row : button;
|
|
2830
|
-
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches("
|
|
2831
|
-
const anchor = family.length > 0 ? family[0] : base.nextElementSibling;
|
|
2806
|
+
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
|
|
2807
|
+
const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
|
|
2832
2808
|
root.insertBefore(entry, anchor);
|
|
2833
2809
|
}
|
|
2834
2810
|
return true;
|
|
@@ -2836,12 +2812,12 @@ window.__ModuleLoader__.load({
|
|
|
2836
2812
|
/**
|
|
2837
2813
|
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
2838
2814
|
* on later React re-renders.
|
|
2839
|
-
* @param
|
|
2815
|
+
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
2840
2816
|
* @returns disposer removing the entry and its observers.
|
|
2841
2817
|
*/
|
|
2842
|
-
function mountSidebarEntry(
|
|
2843
|
-
if (typeof document !== "undefined" && document.querySelector(
|
|
2844
|
-
const entry = createEntry(
|
|
2818
|
+
function mountSidebarEntry$1(options) {
|
|
2819
|
+
if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
|
|
2820
|
+
const entry = createEntry(options);
|
|
2845
2821
|
let root;
|
|
2846
2822
|
let placed = false;
|
|
2847
2823
|
const tryPlace = () => {
|
|
@@ -2858,7 +2834,7 @@ window.__ModuleLoader__.load({
|
|
|
2858
2834
|
}
|
|
2859
2835
|
root ??= sidebarRoot();
|
|
2860
2836
|
if (root === void 0) return;
|
|
2861
|
-
placed = placeEntry(root, entry);
|
|
2837
|
+
placed = placeEntry(root, entry, options);
|
|
2862
2838
|
if (placed) rootObserver.observe(root, {
|
|
2863
2839
|
childList: true,
|
|
2864
2840
|
subtree: true
|
|
@@ -2877,25 +2853,59 @@ window.__ModuleLoader__.load({
|
|
|
2877
2853
|
tryPlace();
|
|
2878
2854
|
return;
|
|
2879
2855
|
}
|
|
2880
|
-
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
2856
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry, options);
|
|
2881
2857
|
});
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2858
|
+
const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
|
|
2859
|
+
const syncActive = () => {
|
|
2860
|
+
if (options.active.isOpen()) entry.dataset.active = "true";
|
|
2861
|
+
else delete entry.dataset.active;
|
|
2862
|
+
};
|
|
2863
|
+
const unsubscribe = options.active.subscribe(syncActive);
|
|
2864
|
+
syncActive();
|
|
2865
|
+
return unsubscribe;
|
|
2866
|
+
})();
|
|
2888
2867
|
tryPlace();
|
|
2889
2868
|
return () => {
|
|
2890
2869
|
waitObserver.disconnect();
|
|
2891
2870
|
rootObserver.disconnect();
|
|
2892
|
-
|
|
2871
|
+
unsubscribeActive?.();
|
|
2893
2872
|
entry.remove();
|
|
2894
2873
|
};
|
|
2895
2874
|
}
|
|
2896
2875
|
//#endregion
|
|
2876
|
+
//#region src/client/sidebar-entry.ts
|
|
2877
|
+
/** Stable data attribute identifying the injected entry row. */
|
|
2878
|
+
const ENTRY_SELECTOR = "[data-dsh-taskboard-entry]";
|
|
2879
|
+
/** Inline icon (matches the shell's 16px nav-icon look). */
|
|
2880
|
+
const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><path d=\"M2 6.5h12M6.5 6.5v7\"/></svg>";
|
|
2881
|
+
/**
|
|
2882
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
2883
|
+
* on later React re-renders.
|
|
2884
|
+
* @param controller - the board controller the entry toggles.
|
|
2885
|
+
* @returns disposer removing the entry and its observers.
|
|
2886
|
+
*/
|
|
2887
|
+
function mountSidebarEntry(controller) {
|
|
2888
|
+
return mountSidebarEntry$1({
|
|
2889
|
+
rowAttribute: "data-dsh-taskboard-entry",
|
|
2890
|
+
rowSelector: ENTRY_SELECTOR,
|
|
2891
|
+
plugin: "task-board",
|
|
2892
|
+
icon: ICON,
|
|
2893
|
+
css: board_module_css_default,
|
|
2894
|
+
label: () => t("entry.label"),
|
|
2895
|
+
onToggle: () => {
|
|
2896
|
+
controller.toggleBoard();
|
|
2897
|
+
},
|
|
2898
|
+
position: "before",
|
|
2899
|
+
familySelectors: ["[data-dsh-taskboard-entry]", "[data-dsh-ssh-entry]"],
|
|
2900
|
+
active: {
|
|
2901
|
+
subscribe: (listener) => controller.subscribe(listener),
|
|
2902
|
+
isOpen: () => controller.getSnapshot().boardOpen
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
//#endregion
|
|
2897
2907
|
//#region \0dsh-css:packages/dsh-task-board/src/client/settings-card.module.css.mjs
|
|
2898
|
-
const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save{transition:none}}";
|
|
2908
|
+
const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_selectWrap{position:relative}.Jh0q7G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Jh0q7G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Jh0q7G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_selectChevronOpen{transform:rotate(180deg)}.Jh0q7G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Jh0q7G_selectPopupOpen{opacity:1;transform:none}.Jh0q7G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Jh0q7G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Jh0q7G_selectOption:hover,.Jh0q7G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Jh0q7G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save,.Jh0q7G_selectChevron,.Jh0q7G_selectChevronOpen,.Jh0q7G_selectPopup{transition:none}}";
|
|
2899
2909
|
const tagId = "@linxin666/dsh-client-ui-task-board/settings-card.module.css";
|
|
2900
2910
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2901
2911
|
const tag = document.createElement("style");
|
|
@@ -2932,7 +2942,18 @@ window.__ModuleLoader__.load({
|
|
|
2932
2942
|
"readOnly": "Jh0q7G_readOnly",
|
|
2933
2943
|
"reset": "Jh0q7G_reset",
|
|
2934
2944
|
"save": "Jh0q7G_save",
|
|
2935
|
-
"select": "Jh0q7G_select"
|
|
2945
|
+
"select": "Jh0q7G_select",
|
|
2946
|
+
"selectButton": "Jh0q7G_selectButton",
|
|
2947
|
+
"selectChevron": "Jh0q7G_selectChevron",
|
|
2948
|
+
"selectChevronOpen": "Jh0q7G_selectChevronOpen",
|
|
2949
|
+
"selectLabel": "Jh0q7G_selectLabel",
|
|
2950
|
+
"selectOption": "Jh0q7G_selectOption",
|
|
2951
|
+
"selectOptionActive": "Jh0q7G_selectOptionActive",
|
|
2952
|
+
"selectOptionSelected": "Jh0q7G_selectOptionSelected",
|
|
2953
|
+
"selectPopup": "Jh0q7G_selectPopup",
|
|
2954
|
+
"selectPopupClose": "Jh0q7G_selectPopupClose",
|
|
2955
|
+
"selectPopupOpen": "Jh0q7G_selectPopupOpen",
|
|
2956
|
+
"selectWrap": "Jh0q7G_selectWrap"
|
|
2936
2957
|
};
|
|
2937
2958
|
//#endregion
|
|
2938
2959
|
//#region src/client/PluginSettingsCard.tsx
|
|
@@ -3038,7 +3059,7 @@ window.__ModuleLoader__.load({
|
|
|
3038
3059
|
children: props.t("settings.readOnly")
|
|
3039
3060
|
}) : null,
|
|
3040
3061
|
props.children,
|
|
3041
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3062
|
+
props.hideFooter === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3042
3063
|
className: settings_card_module_css_default.footer,
|
|
3043
3064
|
children: [
|
|
3044
3065
|
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
@@ -3066,6 +3087,175 @@ window.__ModuleLoader__.load({
|
|
|
3066
3087
|
}) : null]
|
|
3067
3088
|
});
|
|
3068
3089
|
}
|
|
3090
|
+
const NON_SKIN_BODY_MARKERS = /* @__PURE__ */ new Set(["dshSkinCenter", "dshSidebarCollapsed"]);
|
|
3091
|
+
function isSkinActive() {
|
|
3092
|
+
return Object.keys(document.body.dataset).some((key) => key.startsWith("dsh") && !NON_SKIN_BODY_MARKERS.has(key));
|
|
3093
|
+
}
|
|
3094
|
+
const SELECT_CLOSE_MS = 100;
|
|
3095
|
+
/**
|
|
3096
|
+
* The shared dual-mode select control. While an appearance skin is active it
|
|
3097
|
+
* renders the legacy native `<select>` untouched, so element-level skin
|
|
3098
|
+
* selectors keep working; under the default appearance it renders a
|
|
3099
|
+
* self-drawn `role="listbox"` popup whose open/close is transition-animated.
|
|
3100
|
+
* Staged cards reach it through BooleanField/ChoiceField; immediate-apply
|
|
3101
|
+
* editors (the side-card prefs) bind it directly through onEdit.
|
|
3102
|
+
* 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
|
|
3103
|
+
*/
|
|
3104
|
+
function SelectField(props) {
|
|
3105
|
+
const { id, options, value } = props;
|
|
3106
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
3107
|
+
const [closing, setClosing] = (0, react.useState)(false);
|
|
3108
|
+
const [phase, setPhase] = (0, react.useState)("initial");
|
|
3109
|
+
const [activeIndex, setActiveIndex] = (0, react.useState)(0);
|
|
3110
|
+
const closeTimer = (0, react.useRef)(void 0);
|
|
3111
|
+
const wrapRef = (0, react.useRef)(null);
|
|
3112
|
+
const popupRef = (0, react.useRef)(null);
|
|
3113
|
+
const currentIndex = () => {
|
|
3114
|
+
const index = options.findIndex((option) => option.value === value);
|
|
3115
|
+
return index >= 0 ? index : 0;
|
|
3116
|
+
};
|
|
3117
|
+
const close = (0, react.useCallback)(() => {
|
|
3118
|
+
if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
|
|
3119
|
+
setClosing(true);
|
|
3120
|
+
closeTimer.current = setTimeout(() => {
|
|
3121
|
+
setClosing(false);
|
|
3122
|
+
setOpen(false);
|
|
3123
|
+
}, SELECT_CLOSE_MS);
|
|
3124
|
+
}, []);
|
|
3125
|
+
const openPopup = () => {
|
|
3126
|
+
if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
|
|
3127
|
+
setActiveIndex(currentIndex());
|
|
3128
|
+
setPhase("initial");
|
|
3129
|
+
setClosing(false);
|
|
3130
|
+
setOpen(true);
|
|
3131
|
+
};
|
|
3132
|
+
const commit = (index) => {
|
|
3133
|
+
const option = options[index];
|
|
3134
|
+
if (option) props.onEdit(option.value);
|
|
3135
|
+
close();
|
|
3136
|
+
};
|
|
3137
|
+
const onTriggerClick = () => {
|
|
3138
|
+
if (props.disabled) return;
|
|
3139
|
+
if (open && !closing) close();
|
|
3140
|
+
else openPopup();
|
|
3141
|
+
};
|
|
3142
|
+
const onKeyDown = (event) => {
|
|
3143
|
+
if (props.disabled) return;
|
|
3144
|
+
const count = options.length;
|
|
3145
|
+
switch (event.key) {
|
|
3146
|
+
case "ArrowDown":
|
|
3147
|
+
case "ArrowUp":
|
|
3148
|
+
case "Enter":
|
|
3149
|
+
case " ":
|
|
3150
|
+
event.preventDefault();
|
|
3151
|
+
if (!open) openPopup();
|
|
3152
|
+
else if (!closing) if (event.key === "ArrowDown") setActiveIndex((index) => (index + 1) % count);
|
|
3153
|
+
else if (event.key === "ArrowUp") setActiveIndex((index) => (index - 1 + count) % count);
|
|
3154
|
+
else commit(activeIndex);
|
|
3155
|
+
break;
|
|
3156
|
+
case "Escape":
|
|
3157
|
+
if (open) {
|
|
3158
|
+
event.preventDefault();
|
|
3159
|
+
event.stopPropagation();
|
|
3160
|
+
close();
|
|
3161
|
+
}
|
|
3162
|
+
break;
|
|
3163
|
+
case "Tab":
|
|
3164
|
+
if (open) close();
|
|
3165
|
+
break;
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
(0, react.useEffect)(() => () => {
|
|
3169
|
+
if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
|
|
3170
|
+
}, []);
|
|
3171
|
+
(0, react.useLayoutEffect)(() => {
|
|
3172
|
+
if (open && !closing && phase === "initial") {
|
|
3173
|
+
popupRef.current?.offsetHeight;
|
|
3174
|
+
setPhase("open");
|
|
3175
|
+
}
|
|
3176
|
+
}, [
|
|
3177
|
+
open,
|
|
3178
|
+
closing,
|
|
3179
|
+
phase
|
|
3180
|
+
]);
|
|
3181
|
+
(0, react.useEffect)(() => {
|
|
3182
|
+
if (!open) return;
|
|
3183
|
+
const onPointerDown = (event) => {
|
|
3184
|
+
const target = event.target;
|
|
3185
|
+
if (target instanceof Node && !wrapRef.current?.contains(target)) close();
|
|
3186
|
+
};
|
|
3187
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
3188
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
3189
|
+
}, [open, close]);
|
|
3190
|
+
(0, react.useEffect)(() => {
|
|
3191
|
+
if (props.disabled && open) close();
|
|
3192
|
+
}, [
|
|
3193
|
+
props.disabled,
|
|
3194
|
+
open,
|
|
3195
|
+
close
|
|
3196
|
+
]);
|
|
3197
|
+
if (isSkinActive()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
3198
|
+
id,
|
|
3199
|
+
className: settings_card_module_css_default.select,
|
|
3200
|
+
value,
|
|
3201
|
+
disabled: props.disabled,
|
|
3202
|
+
onChange: (event) => {
|
|
3203
|
+
props.onEdit(event.target.value);
|
|
3204
|
+
},
|
|
3205
|
+
children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3206
|
+
value: option.value,
|
|
3207
|
+
children: option.label
|
|
3208
|
+
}, option.value))
|
|
3209
|
+
});
|
|
3210
|
+
const label = options.find((option) => option.value === value)?.label ?? "";
|
|
3211
|
+
const popupClass = closing ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupClose}` : phase === "open" ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupOpen}` : settings_card_module_css_default.selectPopup;
|
|
3212
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3213
|
+
className: settings_card_module_css_default.selectWrap,
|
|
3214
|
+
ref: wrapRef,
|
|
3215
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3216
|
+
type: "button",
|
|
3217
|
+
id,
|
|
3218
|
+
className: `${settings_card_module_css_default.select} ${settings_card_module_css_default.selectButton}`,
|
|
3219
|
+
disabled: props.disabled,
|
|
3220
|
+
"aria-haspopup": "listbox",
|
|
3221
|
+
"aria-expanded": open,
|
|
3222
|
+
"aria-activedescendant": open ? `${id}-o${activeIndex}` : void 0,
|
|
3223
|
+
"aria-invalid": props.invalid || void 0,
|
|
3224
|
+
onClick: onTriggerClick,
|
|
3225
|
+
onKeyDown,
|
|
3226
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3227
|
+
className: settings_card_module_css_default.selectLabel,
|
|
3228
|
+
children: label
|
|
3229
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3230
|
+
width: "14",
|
|
3231
|
+
height: "14",
|
|
3232
|
+
viewBox: "0 0 14 14",
|
|
3233
|
+
fill: "none",
|
|
3234
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
3235
|
+
className: open ? `${settings_card_module_css_default.selectChevron} ${settings_card_module_css_default.selectChevronOpen}` : settings_card_module_css_default.selectChevron,
|
|
3236
|
+
"aria-hidden": "true",
|
|
3237
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
3238
|
+
d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
|
|
3239
|
+
fill: "currentColor"
|
|
3240
|
+
})
|
|
3241
|
+
})]
|
|
3242
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3243
|
+
className: popupClass,
|
|
3244
|
+
role: "listbox",
|
|
3245
|
+
ref: popupRef,
|
|
3246
|
+
children: options.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3247
|
+
id: `${id}-o${index}`,
|
|
3248
|
+
role: "option",
|
|
3249
|
+
"aria-selected": option.value === value,
|
|
3250
|
+
className: `${settings_card_module_css_default.selectOption}${option.value === value ? ` ${settings_card_module_css_default.selectOptionSelected}` : ""}${index === activeIndex && !closing ? ` ${settings_card_module_css_default.selectOptionActive}` : ""}`,
|
|
3251
|
+
onClick: () => {
|
|
3252
|
+
commit(index);
|
|
3253
|
+
},
|
|
3254
|
+
children: option.label
|
|
3255
|
+
}, option.value))
|
|
3256
|
+
}) : null]
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3069
3259
|
/** A staged boolean field: 继承 / 开 / 关. */
|
|
3070
3260
|
function BooleanField(props) {
|
|
3071
3261
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -3091,28 +3281,26 @@ window.__ModuleLoader__.load({
|
|
|
3091
3281
|
})]
|
|
3092
3282
|
}) : null]
|
|
3093
3283
|
}),
|
|
3094
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3284
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
|
|
3095
3285
|
id: props.id,
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
disabled: props.disabled,
|
|
3099
|
-
onChange: (event) => {
|
|
3100
|
-
props.onEdit(event.target.value);
|
|
3101
|
-
},
|
|
3102
|
-
children: [
|
|
3103
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3286
|
+
options: [
|
|
3287
|
+
{
|
|
3104
3288
|
value: "",
|
|
3105
|
-
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3289
|
+
label: props.inheritLabel
|
|
3290
|
+
},
|
|
3291
|
+
{
|
|
3108
3292
|
value: "true",
|
|
3109
|
-
|
|
3110
|
-
}
|
|
3111
|
-
|
|
3293
|
+
label: props.onLabel
|
|
3294
|
+
},
|
|
3295
|
+
{
|
|
3112
3296
|
value: "false",
|
|
3113
|
-
|
|
3114
|
-
}
|
|
3115
|
-
]
|
|
3297
|
+
label: props.offLabel
|
|
3298
|
+
}
|
|
3299
|
+
],
|
|
3300
|
+
value: props.text,
|
|
3301
|
+
disabled: props.disabled,
|
|
3302
|
+
invalid: props.invalid,
|
|
3303
|
+
onEdit: props.onEdit
|
|
3116
3304
|
}),
|
|
3117
3305
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3118
3306
|
className: settings_card_module_css_default.hint,
|
|
@@ -3384,14 +3572,19 @@ window.__ModuleLoader__.load({
|
|
|
3384
3572
|
store;
|
|
3385
3573
|
/** @param scope - the bound settings scope for the `task-board` namespace. */
|
|
3386
3574
|
constructor(scope) {
|
|
3387
|
-
this.form = new CardForm(scope, [
|
|
3575
|
+
this.form = new CardForm(scope, [
|
|
3576
|
+
booleanField("enabled"),
|
|
3577
|
+
booleanField("announceToAgent"),
|
|
3578
|
+
booleanField("preventIdleSleep")
|
|
3579
|
+
]);
|
|
3388
3580
|
this.store = this.form.bind(() => this.projection());
|
|
3389
3581
|
}
|
|
3390
3582
|
projection() {
|
|
3391
3583
|
return {
|
|
3392
3584
|
...this.form.shell(),
|
|
3393
3585
|
enabled: this.form.field("enabled"),
|
|
3394
|
-
announceToAgent: this.form.field("announceToAgent")
|
|
3586
|
+
announceToAgent: this.form.field("announceToAgent"),
|
|
3587
|
+
preventIdleSleep: this.form.field("preventIdleSleep")
|
|
3395
3588
|
};
|
|
3396
3589
|
}
|
|
3397
3590
|
/**
|
|
@@ -3421,6 +3614,21 @@ window.__ModuleLoader__.load({
|
|
|
3421
3614
|
const { t } = props;
|
|
3422
3615
|
const state = props.useTaskBoardSettingsCard((snapshot) => snapshot);
|
|
3423
3616
|
const disabled = !state.writable;
|
|
3617
|
+
const [power, setPower] = (0, react.useState)();
|
|
3618
|
+
(0, react.useEffect)(() => {
|
|
3619
|
+
let live = true;
|
|
3620
|
+
const events = new EventSource("/api/task-board/events");
|
|
3621
|
+
events.onmessage = (message) => {
|
|
3622
|
+
try {
|
|
3623
|
+
const frame = JSON.parse(message.data);
|
|
3624
|
+
if (frame.power !== void 0 && live) setPower(frame.power);
|
|
3625
|
+
} catch {}
|
|
3626
|
+
};
|
|
3627
|
+
return () => {
|
|
3628
|
+
live = false;
|
|
3629
|
+
events.close();
|
|
3630
|
+
};
|
|
3631
|
+
}, []);
|
|
3424
3632
|
const fieldProps = {
|
|
3425
3633
|
overriddenLabel: t("settings.overridden"),
|
|
3426
3634
|
resetLabel: t("settings.reset"),
|
|
@@ -3431,43 +3639,172 @@ window.__ModuleLoader__.load({
|
|
|
3431
3639
|
t,
|
|
3432
3640
|
titleKey: "settings.title",
|
|
3433
3641
|
descriptionKey: "settings.description",
|
|
3642
|
+
defaultOpen: false,
|
|
3434
3643
|
state,
|
|
3435
3644
|
onSave: props.save,
|
|
3436
3645
|
onDiscard: props.discard,
|
|
3437
|
-
children: [
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3646
|
+
children: [
|
|
3647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
3648
|
+
id: "settings-task-board-enabled",
|
|
3649
|
+
label: t("settings.enabled"),
|
|
3650
|
+
hint: t("settings.enabledHint"),
|
|
3651
|
+
inheritLabel: t("settings.inherit"),
|
|
3652
|
+
onLabel: t("settings.on"),
|
|
3653
|
+
offLabel: t("settings.off"),
|
|
3654
|
+
...fieldProps,
|
|
3655
|
+
...state.enabled,
|
|
3656
|
+
onEdit: (text) => {
|
|
3657
|
+
props.edit("enabled", text);
|
|
3658
|
+
},
|
|
3659
|
+
onReset: () => {
|
|
3660
|
+
props.resetField("enabled");
|
|
3661
|
+
}
|
|
3662
|
+
}),
|
|
3663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
3664
|
+
id: "settings-task-board-announce",
|
|
3665
|
+
label: t("settings.announceToAgent"),
|
|
3666
|
+
hint: t("settings.announceToAgentHint"),
|
|
3667
|
+
inheritLabel: t("settings.inherit"),
|
|
3668
|
+
onLabel: t("settings.on"),
|
|
3669
|
+
offLabel: t("settings.off"),
|
|
3670
|
+
...fieldProps,
|
|
3671
|
+
...state.announceToAgent,
|
|
3672
|
+
onEdit: (text) => {
|
|
3673
|
+
props.edit("announceToAgent", text);
|
|
3674
|
+
},
|
|
3675
|
+
onReset: () => {
|
|
3676
|
+
props.resetField("announceToAgent");
|
|
3677
|
+
}
|
|
3678
|
+
}),
|
|
3679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
3680
|
+
id: "settings-task-board-prevent-idle-sleep",
|
|
3681
|
+
label: t("settings.preventIdleSleep"),
|
|
3682
|
+
hint: t("settings.preventIdleSleepHint"),
|
|
3683
|
+
inheritLabel: t("settings.inherit"),
|
|
3684
|
+
onLabel: t("settings.on"),
|
|
3685
|
+
offLabel: t("settings.off"),
|
|
3686
|
+
...fieldProps,
|
|
3687
|
+
...state.preventIdleSleep,
|
|
3688
|
+
onEdit: (text) => {
|
|
3689
|
+
props.edit("preventIdleSleep", text);
|
|
3690
|
+
},
|
|
3691
|
+
onReset: () => {
|
|
3692
|
+
props.resetField("preventIdleSleep");
|
|
3693
|
+
}
|
|
3694
|
+
}),
|
|
3695
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.powerStatus", {
|
|
3696
|
+
platform: power?.platform ?? t("settings.powerUnknown"),
|
|
3697
|
+
phase: power?.phase ?? t("settings.powerUnknown"),
|
|
3698
|
+
running: String(power?.runningSessions ?? 0),
|
|
3699
|
+
schedules: String(power?.armedSchedules ?? 0)
|
|
3700
|
+
}) }),
|
|
3701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.powerBoundary") }),
|
|
3702
|
+
power?.lastError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("settings.powerError", { error: power.lastError }) })
|
|
3703
|
+
]
|
|
3468
3704
|
});
|
|
3469
3705
|
}
|
|
3470
3706
|
//#endregion
|
|
3707
|
+
//#region src/protocol.ts
|
|
3708
|
+
const TASK_BOARD_API_PREFIX = "/api/task-board";
|
|
3709
|
+
//#endregion
|
|
3710
|
+
//#region src/client/host-api.ts
|
|
3711
|
+
const IMPORT_MARKER = "dsh.taskBoard.v2.hostImported";
|
|
3712
|
+
const SOURCE_KEY = "dsh.taskBoard.v2.sourceId";
|
|
3713
|
+
const IMPORT_REQUEST_KEY = "dsh.taskBoard.v2.importRequestId";
|
|
3714
|
+
const REQUEST_TIMEOUT_MS = 15e3;
|
|
3715
|
+
function uuid() {
|
|
3716
|
+
return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
3717
|
+
}
|
|
3718
|
+
async function readJson(response) {
|
|
3719
|
+
const body = await response.json();
|
|
3720
|
+
if (!response.ok) throw new Error(body.error ?? `task-board request failed: ${response.status}`);
|
|
3721
|
+
return body;
|
|
3722
|
+
}
|
|
3723
|
+
var HttpTaskBoardHostTransport = class {
|
|
3724
|
+
storage;
|
|
3725
|
+
constructor(storage = globalThis.localStorage) {
|
|
3726
|
+
this.storage = storage;
|
|
3727
|
+
}
|
|
3728
|
+
async bootstrap(legacy) {
|
|
3729
|
+
const initial = await this.state();
|
|
3730
|
+
const ledgerId = initial.scheduler.ledgerId;
|
|
3731
|
+
if (legacy.length > 0 && ledgerId !== void 0 && this.storage?.getItem(IMPORT_MARKER) !== ledgerId) {
|
|
3732
|
+
let sourceId = this.storage?.getItem(SOURCE_KEY);
|
|
3733
|
+
if (sourceId === null || sourceId === void 0 || sourceId === "") {
|
|
3734
|
+
sourceId = uuid();
|
|
3735
|
+
this.storage?.setItem(SOURCE_KEY, sourceId);
|
|
3736
|
+
}
|
|
3737
|
+
let requestId = this.storage?.getItem(IMPORT_REQUEST_KEY);
|
|
3738
|
+
if (requestId === null || requestId === void 0 || requestId === "") {
|
|
3739
|
+
requestId = uuid();
|
|
3740
|
+
this.storage?.setItem(IMPORT_REQUEST_KEY, requestId);
|
|
3741
|
+
}
|
|
3742
|
+
const snapshot = await this.post(requestId, {
|
|
3743
|
+
kind: "import",
|
|
3744
|
+
sourceId,
|
|
3745
|
+
tasks: [...legacy]
|
|
3746
|
+
});
|
|
3747
|
+
this.storage?.setItem(IMPORT_MARKER, snapshot.scheduler.ledgerId ?? ledgerId);
|
|
3748
|
+
return snapshot;
|
|
3749
|
+
}
|
|
3750
|
+
return initial;
|
|
3751
|
+
}
|
|
3752
|
+
async state() {
|
|
3753
|
+
return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: "no-store" });
|
|
3754
|
+
}
|
|
3755
|
+
async action(action) {
|
|
3756
|
+
return await this.post(uuid(), action);
|
|
3757
|
+
}
|
|
3758
|
+
async post(requestId, action) {
|
|
3759
|
+
const envelope = {
|
|
3760
|
+
requestId,
|
|
3761
|
+
action
|
|
3762
|
+
};
|
|
3763
|
+
return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
|
|
3764
|
+
method: "POST",
|
|
3765
|
+
headers: { "content-type": "application/json" },
|
|
3766
|
+
body: JSON.stringify(envelope)
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
async request(url, init) {
|
|
3770
|
+
const controller = new AbortController();
|
|
3771
|
+
const timeout = globalThis.setTimeout(() => {
|
|
3772
|
+
controller.abort();
|
|
3773
|
+
}, REQUEST_TIMEOUT_MS);
|
|
3774
|
+
try {
|
|
3775
|
+
return await readJson(await fetch(url, {
|
|
3776
|
+
...init,
|
|
3777
|
+
signal: controller.signal
|
|
3778
|
+
}));
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
if (controller.signal.aborted) throw new Error(`task-board Host request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`);
|
|
3781
|
+
throw error;
|
|
3782
|
+
} finally {
|
|
3783
|
+
globalThis.clearTimeout(timeout);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
subscribe(listener) {
|
|
3787
|
+
const events = new EventSource(`${TASK_BOARD_API_PREFIX}/events`);
|
|
3788
|
+
events.onmessage = (message) => {
|
|
3789
|
+
try {
|
|
3790
|
+
const parsed = JSON.parse(message.data);
|
|
3791
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.revision !== "number") throw new Error("invalid event frame");
|
|
3792
|
+
listener(parsed);
|
|
3793
|
+
} catch {
|
|
3794
|
+
listener();
|
|
3795
|
+
}
|
|
3796
|
+
};
|
|
3797
|
+
const onVisible = () => {
|
|
3798
|
+
if (document.visibilityState === "visible") listener();
|
|
3799
|
+
};
|
|
3800
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
3801
|
+
return () => {
|
|
3802
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
3803
|
+
events.close();
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
//#endregion
|
|
3471
3808
|
//#region src/client/index.ts
|
|
3472
3809
|
/** Locale namespace this plugin owns. */
|
|
3473
3810
|
const NS = "task-board";
|
|
@@ -3512,85 +3849,18 @@ window.__ModuleLoader__.load({
|
|
|
3512
3849
|
let uiDisposer;
|
|
3513
3850
|
const mountUi = () => {
|
|
3514
3851
|
if (uiDisposer !== void 0) return;
|
|
3515
|
-
const sessions = ctx.sessions;
|
|
3516
|
-
const workspaces = ctx.workspaces;
|
|
3852
|
+
const sessions = ctx.get("sessions");
|
|
3853
|
+
const workspaces = ctx.get("workspaces");
|
|
3517
3854
|
const connection = ctx.get("connection");
|
|
3518
3855
|
const controller = new BoardController({
|
|
3519
3856
|
store: new LocalStorageTaskStore(),
|
|
3520
|
-
|
|
3521
|
-
sessions: {
|
|
3522
|
-
list: sessions.list,
|
|
3523
|
-
binding: (id) => {
|
|
3524
|
-
const binding = sessions.binding(id);
|
|
3525
|
-
if (binding === void 0) return void 0;
|
|
3526
|
-
const { session } = binding;
|
|
3527
|
-
return { session: {
|
|
3528
|
-
rename: (title) => session.rename(title),
|
|
3529
|
-
prompt: (content, mode) => session.prompt(content, mode).then((result) => result.ok ? { ok: true } : {
|
|
3530
|
-
ok: false,
|
|
3531
|
-
error: result.error
|
|
3532
|
-
}),
|
|
3533
|
-
command: (line) => session.command(line).then((result) => result.ok ? {
|
|
3534
|
-
ok: true,
|
|
3535
|
-
matched: result.value.matched
|
|
3536
|
-
} : {
|
|
3537
|
-
ok: false,
|
|
3538
|
-
error: result.error
|
|
3539
|
-
}),
|
|
3540
|
-
getSnapshot: () => session.getSnapshot(),
|
|
3541
|
-
subscribe: (fn) => session.subscribe(fn)
|
|
3542
|
-
} };
|
|
3543
|
-
},
|
|
3544
|
-
noteAgentPreset: (sessionId, agentPreset) => sessions.noteAgentPreset(sessionId, agentPreset)
|
|
3545
|
-
},
|
|
3546
|
-
workspaces: {
|
|
3547
|
-
list: workspaces.list,
|
|
3548
|
-
connectWorkspace: (id) => workspaces.connectWorkspace(id)
|
|
3549
|
-
},
|
|
3550
|
-
presets: { select: async (sessionId, agentPreset) => {
|
|
3551
|
-
try {
|
|
3552
|
-
const response = await connection.api.agentPresets.select({
|
|
3553
|
-
sessionId,
|
|
3554
|
-
agentPreset
|
|
3555
|
-
});
|
|
3556
|
-
return response.result.ok ? { ok: true } : {
|
|
3557
|
-
ok: false,
|
|
3558
|
-
error: response.result.error
|
|
3559
|
-
};
|
|
3560
|
-
} catch (error) {
|
|
3561
|
-
return {
|
|
3562
|
-
ok: false,
|
|
3563
|
-
error
|
|
3564
|
-
};
|
|
3565
|
-
}
|
|
3566
|
-
} },
|
|
3567
|
-
history: { loadTail: async (sessionId) => {
|
|
3568
|
-
const response = await connection.api.sessions.history({
|
|
3569
|
-
sessionId,
|
|
3570
|
-
maxMessages: 20
|
|
3571
|
-
});
|
|
3572
|
-
return response.result.ok ? { events: response.result.value.events.map((entry) => entry.event) } : void 0;
|
|
3573
|
-
} }
|
|
3574
|
-
}),
|
|
3857
|
+
transport: new HttpTaskBoardHostTransport(),
|
|
3575
3858
|
sessions: {
|
|
3576
3859
|
list: sessions.list,
|
|
3577
3860
|
open: (id) => sessions.open(id)
|
|
3578
3861
|
}
|
|
3579
3862
|
});
|
|
3580
3863
|
controller.start();
|
|
3581
|
-
const scheduler = new SchedulerService({
|
|
3582
|
-
tasks: () => controller.getSnapshot().tasks,
|
|
3583
|
-
refresh: () => controller.reloadFromStore(),
|
|
3584
|
-
now: () => Date.now(),
|
|
3585
|
-
runTask: (id) => controller.runTask(id),
|
|
3586
|
-
applySchedule: (id, nextRunAt, lastTriggeredAt) => controller.applyScheduleNextRun(id, nextRunAt, lastTriggeredAt),
|
|
3587
|
-
ready: () => sessions.list.getSnapshot().phase === "ready",
|
|
3588
|
-
environment: {
|
|
3589
|
-
addEventListener: (type, listener) => document.addEventListener(type, listener),
|
|
3590
|
-
removeEventListener: (type, listener) => document.removeEventListener(type, listener)
|
|
3591
|
-
}
|
|
3592
|
-
});
|
|
3593
|
-
scheduler.start();
|
|
3594
3864
|
const disposers = [];
|
|
3595
3865
|
const pushWorkspaceOptions = () => {
|
|
3596
3866
|
const snapshot = workspaces.list.getSnapshot();
|
|
@@ -3628,7 +3898,6 @@ window.__ModuleLoader__.load({
|
|
|
3628
3898
|
}
|
|
3629
3899
|
uiDisposer = () => {
|
|
3630
3900
|
for (const dispose of disposers.splice(0)) dispose();
|
|
3631
|
-
scheduler.dispose();
|
|
3632
3901
|
controller.dispose();
|
|
3633
3902
|
uiDisposer = void 0;
|
|
3634
3903
|
};
|