dsh-autorestart 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +16 -152
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -93,6 +93,7 @@ window.__ModuleLoader__.load({
|
|
|
93
93
|
parkBusy: '停泊中…',
|
|
94
94
|
parkError: '停泊失败:{message}',
|
|
95
95
|
tooltip: '等待其他会话空闲后自动重启;重启后会自动继续本会话进度',
|
|
96
|
+
tooltipBusy: '等待其他会话空闲后自动重启(计数含“排队但还没开始”的任务);重启后本会话会自动继续进度',
|
|
96
97
|
tooltipWithError: '等待其他会话空闲后自动重启;重启后会自动继续本会话进度。{message}',
|
|
97
98
|
dotAria: '待重启',
|
|
98
99
|
panelHeading: '待重启',
|
|
@@ -127,6 +128,8 @@ window.__ModuleLoader__.load({
|
|
|
127
128
|
parkError: 'Park failed: {message}',
|
|
128
129
|
tooltip:
|
|
129
130
|
'Waits for the other sessions to become idle, then restarts automatically; this session resumes its progress after the restart.',
|
|
131
|
+
tooltipBusy:
|
|
132
|
+
'Waits for the other sessions to become idle, then restarts automatically (the count includes work that is queued but not started yet); this session resumes its progress after the restart.',
|
|
130
133
|
tooltipWithError:
|
|
131
134
|
'Waits for the other sessions to become idle, then restarts automatically; this session resumes its progress after the restart. {message}',
|
|
132
135
|
dotAria: 'Pending restart',
|
|
@@ -492,13 +495,11 @@ window.__ModuleLoader__.load({
|
|
|
492
495
|
const sessionId = props && props.sessionId !== undefined ? props.sessionId : null
|
|
493
496
|
const hasSession = sessionId !== null && sessionId !== ''
|
|
494
497
|
const info = useSharedInfo(hasSession)
|
|
495
|
-
const [open, setOpen] = useState(false)
|
|
496
498
|
// The session id of the goal that is currently being parked, or null.
|
|
497
499
|
const [parking, setParking] = useState(null)
|
|
498
500
|
// The message of the last failed park request (the Host's own message).
|
|
501
|
+
// It is shown in the pill's tooltip: the pill has no details panel.
|
|
499
502
|
const [parkError, setParkError] = useState('')
|
|
500
|
-
const [, setTick] = useState(0)
|
|
501
|
-
const wrapperRef = useRef(null)
|
|
502
503
|
const t = translate ?? ((name) => name)
|
|
503
504
|
|
|
504
505
|
const pending = Boolean(info && info.pendingRestart === true)
|
|
@@ -506,43 +507,14 @@ window.__ModuleLoader__.load({
|
|
|
506
507
|
// happens as soon as this poll cycle ends.
|
|
507
508
|
const pulsing = pending && Boolean(info.allowRestart === true) && !prefersReducedMotion()
|
|
508
509
|
|
|
509
|
-
//
|
|
510
|
+
// A park error never outlives the pending state.
|
|
510
511
|
useEffect(() => {
|
|
511
512
|
if (!pending) {
|
|
512
|
-
setOpen(false)
|
|
513
513
|
setParkError('')
|
|
514
514
|
setParking(null)
|
|
515
515
|
}
|
|
516
516
|
}, [pending])
|
|
517
517
|
|
|
518
|
-
// Clicking outside the panel or pressing Escape closes it. Both
|
|
519
|
-
// listeners are removed when the panel closes and on unmount.
|
|
520
|
-
useEffect(() => {
|
|
521
|
-
if (!open) return undefined
|
|
522
|
-
const onPointerDown = (event) => {
|
|
523
|
-
const node = wrapperRef.current
|
|
524
|
-
if (node && event && event.target && typeof node.contains === 'function'
|
|
525
|
-
&& node.contains(event.target)) return
|
|
526
|
-
setOpen(false)
|
|
527
|
-
}
|
|
528
|
-
const onKeyDown = (event) => {
|
|
529
|
-
if (event && event.key === 'Escape') setOpen(false)
|
|
530
|
-
}
|
|
531
|
-
document.addEventListener('mousedown', onPointerDown, true)
|
|
532
|
-
document.addEventListener('keydown', onKeyDown, true)
|
|
533
|
-
return () => {
|
|
534
|
-
document.removeEventListener('mousedown', onPointerDown, true)
|
|
535
|
-
document.removeEventListener('keydown', onKeyDown, true)
|
|
536
|
-
}
|
|
537
|
-
}, [open])
|
|
538
|
-
|
|
539
|
-
// A one-second tick keeps the "waiting for" duration in the panel fresh.
|
|
540
|
-
useEffect(() => {
|
|
541
|
-
if (!open) return undefined
|
|
542
|
-
const timer = setInterval(() => setTick((value) => value + 1), 1000)
|
|
543
|
-
return () => clearInterval(timer)
|
|
544
|
-
}, [open])
|
|
545
|
-
|
|
546
518
|
if (!info || info.pendingRestart !== true) return null
|
|
547
519
|
|
|
548
520
|
const waiters = Array.isArray(info.waiters) ? info.waiters : []
|
|
@@ -551,15 +523,22 @@ window.__ModuleLoader__.load({
|
|
|
551
523
|
if (!waiting) return null
|
|
552
524
|
|
|
553
525
|
const blockedBy = Array.isArray(info.blockedBy) ? info.blockedBy : []
|
|
554
|
-
|
|
526
|
+
// One entry per SESSION: a conversation that is running *and* still has
|
|
527
|
+
// queued work is one conversation, so the pill never counts it twice.
|
|
528
|
+
const seenSessions = new Set()
|
|
529
|
+
const others = blockedBy.filter((entry) => {
|
|
530
|
+
if (!entry) return false
|
|
531
|
+
const id = String(entry.sessionId)
|
|
532
|
+
if (id === key || seenSessions.has(id)) return false
|
|
533
|
+
seenSessions.add(id)
|
|
534
|
+
return true
|
|
535
|
+
})
|
|
555
536
|
// A goal-driven blocker: an armed goal that keeps driving rounds by
|
|
556
537
|
// itself (`goal-active`, parkable) or a goal whose round budget is used
|
|
557
538
|
// up (`goal-exhausted`, not parkable at all).
|
|
558
539
|
const goalActive = others.filter((entry) => String(entry.reason) === 'goal-active')
|
|
559
540
|
const goalExhausted = others.filter((entry) => String(entry.reason) === 'goal-exhausted')
|
|
560
541
|
const otherBusy = others.length - goalActive.length - goalExhausted.length
|
|
561
|
-
// The goals the Host already parked: they continue after the restart.
|
|
562
|
-
const parkedGoals = Array.isArray(info.parkedGoals) ? info.parkedGoals : []
|
|
563
542
|
|
|
564
543
|
const status = goalActive.length > 0
|
|
565
544
|
? t('goalActive')
|
|
@@ -571,16 +550,6 @@ window.__ModuleLoader__.load({
|
|
|
571
550
|
|
|
572
551
|
if (pulsing) ensurePulseStyle()
|
|
573
552
|
|
|
574
|
-
const toggle = () => setOpen((value) => !value)
|
|
575
|
-
const onKeyDown = (event) => {
|
|
576
|
-
if (!event) return
|
|
577
|
-
const pressed = event.key
|
|
578
|
-
if (pressed === 'Enter' || pressed === ' ' || pressed === 'Spacebar') {
|
|
579
|
-
event.preventDefault()
|
|
580
|
-
toggle()
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
553
|
/**
|
|
585
554
|
* Park the goal of one blocking session and ask for the restart now.
|
|
586
555
|
*
|
|
@@ -612,7 +581,6 @@ window.__ModuleLoader__.load({
|
|
|
612
581
|
? (body.error || body.message || '')
|
|
613
582
|
: ''
|
|
614
583
|
setParkError(fill(t('parkError'), { message: message || String(response.status) }))
|
|
615
|
-
setOpen(true)
|
|
616
584
|
return
|
|
617
585
|
}
|
|
618
586
|
if (body && Array.isArray(body.reported)) {
|
|
@@ -621,96 +589,18 @@ window.__ModuleLoader__.load({
|
|
|
621
589
|
)
|
|
622
590
|
if (reported) {
|
|
623
591
|
setParkError(fill(t('parkError'), { message: String(reported.reason || 'not-parked') }))
|
|
624
|
-
setOpen(true)
|
|
625
592
|
}
|
|
626
593
|
}
|
|
627
594
|
} catch (error) {
|
|
628
595
|
setParkError(fill(t('parkError'), { message: describeError(error) }))
|
|
629
|
-
setOpen(true)
|
|
630
596
|
} finally {
|
|
631
597
|
setParking(null)
|
|
632
598
|
}
|
|
633
599
|
}
|
|
634
600
|
|
|
635
|
-
const pendingSince = stateStore.pendingSince
|
|
636
|
-
const waitingMs = pendingSince === null || pendingSince === undefined
|
|
637
|
-
? 0
|
|
638
|
-
: Math.max(0, Date.now() - pendingSince)
|
|
639
|
-
|
|
640
|
-
const panelLines = [
|
|
641
|
-
h('div', { key: 'heading', style: PANEL_HEADING_STYLE }, t('panelHeading')),
|
|
642
|
-
h(
|
|
643
|
-
'div',
|
|
644
|
-
{ key: 'state', style: PANEL_LINE_STYLE },
|
|
645
|
-
goalActive.length > 0
|
|
646
|
-
? t('panelStateGoalActive')
|
|
647
|
-
: goalExhausted.length > 0
|
|
648
|
-
? t('panelStateGoalExhausted')
|
|
649
|
-
: otherBusy > 0
|
|
650
|
-
? t('panelStateBusy')
|
|
651
|
-
: t('panelStateIdle'),
|
|
652
|
-
),
|
|
653
|
-
]
|
|
654
|
-
if (parkError) {
|
|
655
|
-
panelLines.push(h('div', { key: 'error', style: PANEL_ERROR_STYLE }, parkError))
|
|
656
|
-
}
|
|
657
|
-
others.forEach((entry, index) => {
|
|
658
|
-
const title = entry && typeof entry.title === 'string' ? entry.title.trim() : ''
|
|
659
|
-
const reason = entry && typeof entry.reason === 'string' ? entry.reason : ''
|
|
660
|
-
const name = title || String(entry && entry.sessionId)
|
|
661
|
-
if (reason === 'goal-active') {
|
|
662
|
-
panelLines.push(h(
|
|
663
|
-
'div',
|
|
664
|
-
{ key: 'blocked-' + index, style: PANEL_LINE_STYLE },
|
|
665
|
-
fill(t('panelGoalActiveLine'), { title: name }),
|
|
666
|
-
))
|
|
667
|
-
return
|
|
668
|
-
}
|
|
669
|
-
if (reason === 'goal-exhausted') {
|
|
670
|
-
panelLines.push(h(
|
|
671
|
-
'div',
|
|
672
|
-
{ key: 'blocked-' + index, style: PANEL_LINE_STYLE },
|
|
673
|
-
fill(t('panelGoalExhaustedLine'), { title: name }),
|
|
674
|
-
))
|
|
675
|
-
return
|
|
676
|
-
}
|
|
677
|
-
panelLines.push(h(
|
|
678
|
-
'div',
|
|
679
|
-
{ key: 'blocked-' + index, style: PANEL_LINE_STYLE },
|
|
680
|
-
title ? fill(t('panelBlockedTitle'), { title }) : t('panelBlockedGeneric'),
|
|
681
|
-
))
|
|
682
|
-
})
|
|
683
|
-
if (parkedGoals.length > 0) {
|
|
684
|
-
panelLines.push(h(
|
|
685
|
-
'div',
|
|
686
|
-
{ key: 'parked-title', style: PANEL_MUTED_STYLE },
|
|
687
|
-
t('panelParkedTitle'),
|
|
688
|
-
))
|
|
689
|
-
parkedGoals.forEach((entry, index) => {
|
|
690
|
-
const parkedSession = entry && entry.sessionId != null ? String(entry.sessionId) : ''
|
|
691
|
-
const parkedGoal = entry && entry.goalId != null ? String(entry.goalId) : '-'
|
|
692
|
-
panelLines.push(h(
|
|
693
|
-
'div',
|
|
694
|
-
{ key: 'parked-' + index, style: PANEL_LINE_STYLE },
|
|
695
|
-
fill(t('panelParkedGoal'), { session: parkedSession, goal: parkedGoal }),
|
|
696
|
-
))
|
|
697
|
-
})
|
|
698
|
-
}
|
|
699
|
-
panelLines.push(h(
|
|
700
|
-
'div',
|
|
701
|
-
{ key: 'waiting', style: PANEL_LINE_STYLE },
|
|
702
|
-
fill(t('panelWaiting'), { duration: formatWaiting(waitingMs, t) }),
|
|
703
|
-
))
|
|
704
|
-
panelLines.push(h(
|
|
705
|
-
'div',
|
|
706
|
-
{ key: 'resume', style: PANEL_MUTED_STYLE },
|
|
707
|
-
t('panelResume'),
|
|
708
|
-
))
|
|
709
|
-
|
|
710
601
|
return h(
|
|
711
602
|
'div',
|
|
712
603
|
{
|
|
713
|
-
ref: wrapperRef,
|
|
714
604
|
style: WRAPPER_STYLE,
|
|
715
605
|
role: 'status',
|
|
716
606
|
'aria-live': 'polite',
|
|
@@ -721,13 +611,7 @@ window.__ModuleLoader__.load({
|
|
|
721
611
|
style: PILL_STYLE,
|
|
722
612
|
title: parkError
|
|
723
613
|
? fill(t('tooltipWithError'), { message: parkError })
|
|
724
|
-
: t('tooltip'),
|
|
725
|
-
role: 'button',
|
|
726
|
-
tabIndex: 0,
|
|
727
|
-
'aria-haspopup': 'dialog',
|
|
728
|
-
'aria-expanded': open ? 'true' : 'false',
|
|
729
|
-
onClick: toggle,
|
|
730
|
-
onKeyDown,
|
|
614
|
+
: (otherBusy > 0 ? t('tooltipBusy') : t('tooltip')),
|
|
731
615
|
},
|
|
732
616
|
h('span', { style: dotStyle(DOT_STYLE, pulsing), 'aria-hidden': 'true' }),
|
|
733
617
|
h('span', { style: TEXT_STYLE }, h('b', null, t('label')), status),
|
|
@@ -737,7 +621,6 @@ window.__ModuleLoader__.load({
|
|
|
737
621
|
const target = String(entry.sessionId)
|
|
738
622
|
const busy = parking === target
|
|
739
623
|
const onParkClick = (event) => {
|
|
740
|
-
// The button must not toggle the details panel of the pill.
|
|
741
624
|
if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
742
625
|
void parkAndRestart(target)
|
|
743
626
|
}
|
|
@@ -745,7 +628,6 @@ window.__ModuleLoader__.load({
|
|
|
745
628
|
if (!event) return
|
|
746
629
|
const pressed = event.key
|
|
747
630
|
if (pressed === 'Enter' || pressed === ' ' || pressed === 'Spacebar') {
|
|
748
|
-
// Same here: the pill's own Enter / Space handler must not run.
|
|
749
631
|
if (typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
750
632
|
}
|
|
751
633
|
}
|
|
@@ -764,25 +646,7 @@ window.__ModuleLoader__.load({
|
|
|
764
646
|
busy ? t('parkBusy') : t('parkButton'),
|
|
765
647
|
)
|
|
766
648
|
}),
|
|
767
|
-
parkedGoals.length > 0
|
|
768
|
-
? h(
|
|
769
|
-
'span',
|
|
770
|
-
{ style: PARKED_LINE_STYLE },
|
|
771
|
-
fill(t('parkedGoals'), { n: parkedGoals.length }),
|
|
772
|
-
)
|
|
773
|
-
: null,
|
|
774
649
|
),
|
|
775
|
-
open
|
|
776
|
-
? h(
|
|
777
|
-
'div',
|
|
778
|
-
{
|
|
779
|
-
style: PANEL_STYLE,
|
|
780
|
-
role: 'dialog',
|
|
781
|
-
'aria-label': t('panelHeading'),
|
|
782
|
-
},
|
|
783
|
-
panelLines,
|
|
784
|
-
)
|
|
785
|
-
: null,
|
|
786
650
|
)
|
|
787
651
|
}
|
|
788
652
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-autorestart",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepSeek Harness plugin: restart without losing work. Waits until every conversation is idle, restarts the host, then automatically resumes the interrupted sessions. AI-generated, not human-reviewed: use with caution.",
|
|
6
6
|
"license": "MIT",
|