dsh-monkeyai-sessions 0.1.7 → 0.1.9
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.md +1 -1
- package/README.zh.md +1 -1
- package/client.js +466 -329
- package/lib/client-runtime.cjs +466 -329
- package/package.json +1 -1
- package/src/client/index.js +233 -128
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -28,12 +28,19 @@ const t = {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const style = {
|
|
31
|
-
page: { display: 'grid', gap:
|
|
31
|
+
page: { display: 'grid', gap: 14, maxWidth: 1320, margin: '0 auto', padding: '18px 20px 24px', color: t.text, background: '#f7f8fb', height: '100%', minHeight: 0, overflowY: 'auto', boxSizing: 'border-box', alignContent: 'start', fontSize: 13, lineHeight: 1.5 },
|
|
32
32
|
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, paddingBottom: 2 },
|
|
33
|
-
title: { margin: 0, fontSize:
|
|
33
|
+
title: { margin: 0, fontSize: 18, fontWeight: 700, letterSpacing: '-0.025em', color: '#182230' },
|
|
34
|
+
eyebrow: { margin: 0, fontSize: 10.5, fontWeight: 700, letterSpacing: '0.1em', color: t.text3, textTransform: 'uppercase' },
|
|
34
35
|
subtitle: { margin: 0, fontSize: 12, color: t.text3 },
|
|
35
36
|
sectionTitle: { margin: 0, fontSize: 12, fontWeight: 650, color: t.text2, textTransform: 'uppercase', letterSpacing: '0.04em' },
|
|
36
|
-
card: { display: 'grid', gap: 10, padding: 14, border: `1px solid ${t.border}`, borderRadius:
|
|
37
|
+
card: { display: 'grid', gap: 10, padding: 14, border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, boxShadow: '0 2px 8px rgba(16,24,40,0.035)' },
|
|
38
|
+
toolbarCard: { display: 'grid', gap: 10, padding: '13px 15px', border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, boxShadow: '0 2px 8px rgba(16,24,40,0.035)' },
|
|
39
|
+
workspace: { display: 'grid', gridTemplateColumns: 'minmax(248px, 292px) minmax(0, 1fr)', gap: 12, minWidth: 0, alignItems: 'stretch' },
|
|
40
|
+
sessionSidebar: { display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr) auto', gap: 10, minWidth: 0, padding: 12, border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, boxShadow: '0 2px 8px rgba(16,24,40,0.035)' },
|
|
41
|
+
sessionList: { display: 'grid', gridTemplateColumns: '1fr', alignContent: 'start', gap: 6, minHeight: 0, overflowY: 'auto', padding: '1px 3px 2px 1px' },
|
|
42
|
+
thread: { display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr) auto', gap: 10, minWidth: 0, height: 'min(70vh, 720px)', minHeight: 440, overflow: 'hidden', padding: 12 },
|
|
43
|
+
emptyThread: { display: 'grid', placeContent: 'center', justifyItems: 'center', gap: 9, minWidth: 0, minHeight: 'min(70vh, 720px)', padding: 28, border: `1px dashed ${t.border2}`, borderRadius: 12, background: 'linear-gradient(145deg, #ffffff 0%, #f8faff 100%)', textAlign: 'center' },
|
|
37
44
|
row: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
|
|
38
45
|
label: { display: 'grid', gap: 4, flex: 1, minWidth: 180, fontSize: 12, fontWeight: 600, color: t.text2 },
|
|
39
46
|
input: { width: '100%', boxSizing: 'border-box', padding: '7px 10px', borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: 'none', fontSize: 13, transition: 'border-color .15s, box-shadow .15s' },
|
|
@@ -52,13 +59,13 @@ const style = {
|
|
|
52
59
|
sessionTitle: { fontSize: 13, fontWeight: 650, lineHeight: 1.35, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
53
60
|
sessionMeta: { fontSize: 11.5, color: t.text3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
54
61
|
chip: { display: 'inline-flex', alignItems: 'center', gap: 5, padding: '2px 8px', borderRadius: 999, border: `1px solid ${t.border}`, background: t.surfaceAlt, fontSize: 11.5 },
|
|
55
|
-
events: { display: 'flex', flexDirection: 'column', gap:
|
|
62
|
+
events: { display: 'flex', flexDirection: 'column', gap: 10, height: 'auto', minHeight: 0, overflowY: 'auto', padding: '6px 8px 8px 4px', overscrollBehavior: 'contain' },
|
|
56
63
|
event: { padding: '8px 11px', borderRadius: t.radius, background: t.surfaceAlt, border: `1px solid ${t.border}`, overflowWrap: 'anywhere', color: t.text },
|
|
57
64
|
bubbleWrap: { display: 'flex', gap: 8, alignItems: 'flex-start', width: '100%' },
|
|
58
65
|
bubbleWrapUser: { display: 'flex', gap: 8, alignItems: 'flex-start', justifyContent: 'flex-end', width: '100%' },
|
|
59
66
|
avatar: { width: 24, height: 24, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700, color: '#ffffff', flexShrink: 0, marginTop: 2 },
|
|
60
|
-
bubble: { maxWidth: '88%', minWidth: 0, flex: '0 1 auto', width: 'fit-content', borderRadius:
|
|
61
|
-
bubbleUser: { maxWidth: '88%', minWidth: 0, flex: '0 1 auto', width: 'fit-content', borderRadius:
|
|
67
|
+
bubble: { maxWidth: '88%', minWidth: 0, flex: '0 1 auto', width: 'fit-content', borderRadius: 12, padding: '9px 13px', background: t.surface, border: `1px solid ${t.border}`, boxShadow: '0 1px 2px rgba(16,24,40,0.04)', lineHeight: 1.6, fontSize: 13 },
|
|
68
|
+
bubbleUser: { maxWidth: '88%', minWidth: 0, flex: '0 1 auto', width: 'fit-content', borderRadius: 12, padding: '9px 13px', background: '#315fe8', color: '#ffffff', boxShadow: '0 2px 5px rgba(49,95,232,0.18)', lineHeight: 1.6, fontSize: 13 },
|
|
62
69
|
thought: { maxWidth: '88%', borderRadius: t.radiusLg, padding: '7px 11px', background: '#faf7fd', border: `1px solid #e8ddf5`, lineHeight: 1.5, fontSize: 12.5, color: '#5b4a72' },
|
|
63
70
|
thoughtSummary: { cursor: 'pointer', color: '#6b4fa0', fontWeight: 650, fontSize: 10.5, letterSpacing: '0.05em', textTransform: 'uppercase', userSelect: 'none' },
|
|
64
71
|
approval: { padding: '11px 13px', borderRadius: t.radiusLg, border: `1px solid #f0d98c`, background: '#fffbeb', display: 'grid', gap: 8 },
|
|
@@ -82,6 +89,36 @@ const PANEL_CSS = `
|
|
|
82
89
|
.mai-panel [data-events]::-webkit-scrollbar-thumb:hover { background: #b8c2cf; }
|
|
83
90
|
.mai-panel .mai-card-hover { transition: border-color .15s, background .15s; }
|
|
84
91
|
.mai-panel .mai-card-hover:not(:disabled):hover { border-color: #b9cdf3 !important; background: #f7faff !important; }
|
|
92
|
+
.mai-panel .mai-toolbar { position: relative; overflow: hidden; }
|
|
93
|
+
.mai-panel .mai-toolbar::before { content: ''; position: absolute; inset: 0 0 auto; height: 3px; background: linear-gradient(90deg, #3568e8 0%, #8baaf4 46%, #e7edfc 100%); }
|
|
94
|
+
.mai-panel .mai-workspace { display: grid; grid-template-columns: minmax(248px, 292px) minmax(0, 1fr); gap: 12px; min-width: 0; align-items: stretch; }
|
|
95
|
+
.mai-panel .mai-session-sidebar { min-height: min(70vh, 720px); }
|
|
96
|
+
.mai-panel .mai-session-list { scrollbar-gutter: stable; }
|
|
97
|
+
.mai-panel .mai-session-card { position: relative; }
|
|
98
|
+
.mai-panel .mai-session-card[aria-current='true']::before { content: ''; position: absolute; left: 0; top: 9px; bottom: 9px; width: 3px; border-radius: 0 3px 3px 0; background: #3568e8; }
|
|
99
|
+
.mai-panel .mai-thread { height: min(70vh, 720px); min-height: 440px; }
|
|
100
|
+
.mai-panel .mai-thread-events { height: auto !important; min-height: 0 !important; }
|
|
101
|
+
.mai-panel .mai-empty-thread { min-height: min(70vh, 720px); }
|
|
102
|
+
.mai-panel .mai-empty-mark { display: grid; place-items: center; width: 54px; height: 54px; border-radius: 17px; background: #edf2ff; color: #315fe8; box-shadow: inset 0 0 0 1px #dce6ff; }
|
|
103
|
+
.mai-panel .mai-session-meta { overflow-wrap: anywhere; }
|
|
104
|
+
@media (max-width: 900px) {
|
|
105
|
+
.mai-panel .mai-workspace { grid-template-columns: minmax(0, 1fr); }
|
|
106
|
+
.mai-panel .mai-session-sidebar { min-height: 0; grid-template-rows: auto minmax(0, auto) auto; }
|
|
107
|
+
.mai-panel .mai-session-list { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); max-height: 235px; }
|
|
108
|
+
.mai-panel .mai-thread { height: 68vh; min-height: 390px; }
|
|
109
|
+
.mai-panel .mai-empty-thread { min-height: 330px; }
|
|
110
|
+
}
|
|
111
|
+
@media (max-width: 560px) {
|
|
112
|
+
.mai-panel .mai-page-title { font-size: 16px !important; }
|
|
113
|
+
.mai-panel .mai-toolbar-controls { align-items: stretch; }
|
|
114
|
+
.mai-panel .mai-toolbar-controls select { flex-basis: 100% !important; }
|
|
115
|
+
.mai-panel .mai-session-list { grid-template-columns: 1fr !important; max-height: 210px; }
|
|
116
|
+
.mai-panel .mai-thread { height: 68vh; min-height: 360px; padding: 10px; }
|
|
117
|
+
.mai-panel .mai-thread-header { gap: 6px !important; }
|
|
118
|
+
.mai-panel .mai-thread-header strong { min-width: 100px; }
|
|
119
|
+
.mai-panel .mai-thread-header select { flex-basis: 100% !important; }
|
|
120
|
+
.mai-panel .mai-events { gap: 7px; }
|
|
121
|
+
}
|
|
85
122
|
@keyframes mai-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
86
123
|
.mai-panel .mai-pulse { animation: mai-pulse 1.2s ease-in-out infinite; }
|
|
87
124
|
.mai-panel .mai-md > div:first-child > :first-child { margin-top: 0; }
|
|
@@ -94,6 +131,20 @@ function errorText(error) {
|
|
|
94
131
|
return typeof error?.message === 'string' ? error.message : typeof error === 'string' ? error : '远程操作失败'
|
|
95
132
|
}
|
|
96
133
|
|
|
134
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const timer = setTimeout(() => reject(new Error(`${label}超时,请重试`)), timeoutMs)
|
|
137
|
+
Promise.resolve(promise).then(
|
|
138
|
+
value => { clearTimeout(timer); resolve(value) },
|
|
139
|
+
error => { clearTimeout(timer); reject(error) },
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isRemoteStreamCarrierError(error) {
|
|
145
|
+
return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error))
|
|
146
|
+
}
|
|
147
|
+
|
|
97
148
|
// Lightweight inline markdown: fenced code blocks, `code`, **bold**,
|
|
98
149
|
// headings and bullet lists. Everything else renders as plain text so
|
|
99
150
|
// remote content can never inject markup.
|
|
@@ -299,6 +350,7 @@ function RemoteSessionPage({ api }) {
|
|
|
299
350
|
const authWindowRef = useRef(null)
|
|
300
351
|
const eventsRef = useRef(null)
|
|
301
352
|
const prevScrollHeightRef = useRef(null)
|
|
353
|
+
const streamGenerationRef = useRef(0)
|
|
302
354
|
const title = useMemo(() => {
|
|
303
355
|
const runtime = runtimeSessions.find(item => item.id === sessionId)
|
|
304
356
|
const summary = summarySessions.find(item => item.id === sessionId)
|
|
@@ -320,6 +372,7 @@ function RemoteSessionPage({ api }) {
|
|
|
320
372
|
})
|
|
321
373
|
return () => {
|
|
322
374
|
active = false
|
|
375
|
+
streamGenerationRef.current += 1
|
|
323
376
|
streamRef.current?.dispose()
|
|
324
377
|
streamRef.current = null
|
|
325
378
|
}
|
|
@@ -489,6 +542,7 @@ function RemoteSessionPage({ api }) {
|
|
|
489
542
|
setError('')
|
|
490
543
|
setLoading(true)
|
|
491
544
|
try {
|
|
545
|
+
streamGenerationRef.current += 1
|
|
492
546
|
const result = await unwrap(api.listDevices({ accountId, page: 1 }))
|
|
493
547
|
setDevices(result.items)
|
|
494
548
|
const first = result.items.length > 0 ? result.items[0].machineId : ''
|
|
@@ -513,25 +567,33 @@ function RemoteSessionPage({ api }) {
|
|
|
513
567
|
setError('')
|
|
514
568
|
setLoading(true)
|
|
515
569
|
setSessionId('')
|
|
570
|
+
streamGenerationRef.current += 1
|
|
516
571
|
streamRef.current?.dispose()
|
|
517
572
|
streamRef.current = null
|
|
518
573
|
setFrames([])
|
|
519
574
|
setPendingRequests([])
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
575
|
+
try {
|
|
576
|
+
const results = await Promise.allSettled([
|
|
577
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 }))), 50_000, '会话摘要读取'),
|
|
578
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listRuntimeSessions({ accountId, machineId: mid }))), 50_000, 'ACP 会话列表读取'),
|
|
579
|
+
])
|
|
580
|
+
const errors = []
|
|
581
|
+
if (results[0].status === 'fulfilled') setSummarySessions(results[0].value.items)
|
|
582
|
+
else errors.push(`会话摘要索引:${errorText(results[0].reason)}`)
|
|
583
|
+
if (results[1].status === 'fulfilled') setRuntimeSessions(results[1].value.sessions)
|
|
584
|
+
else errors.push(`ACP 会话列表:${errorText(results[1].reason)}`)
|
|
585
|
+
setStatus(errors.length === 0 ? '会话已刷新' : '部分会话来源不可用')
|
|
586
|
+
setError(errors.join('\n'))
|
|
587
|
+
} catch (reason) {
|
|
588
|
+
setStatus('会话刷新失败')
|
|
589
|
+
setError(errorText(reason))
|
|
590
|
+
} finally {
|
|
591
|
+
setLoading(false)
|
|
592
|
+
}
|
|
532
593
|
}
|
|
533
594
|
|
|
534
595
|
async function openSession(session) {
|
|
596
|
+
const generation = ++streamGenerationRef.current
|
|
535
597
|
streamRef.current?.dispose()
|
|
536
598
|
streamRef.current = null
|
|
537
599
|
historyLoadedRef.current = false
|
|
@@ -548,90 +610,113 @@ function RemoteSessionPage({ api }) {
|
|
|
548
610
|
setVisibleCount(RECENT_LIMIT)
|
|
549
611
|
setHistoryLoading(true)
|
|
550
612
|
setStatus('正在连接远程会话')
|
|
551
|
-
let
|
|
552
|
-
|
|
553
|
-
stream
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (frame.type === 'reconnecting') setStatus(frame.message || '正在重连远程 runtime')
|
|
560
|
-
if (frame.type === 'history_loaded') {
|
|
561
|
-
historyLoadedRef.current = true
|
|
562
|
-
setStatus('历史已加载')
|
|
563
|
-
setHistoryLoading(false)
|
|
564
|
-
setSessionStatus(current => current || 'idle')
|
|
565
|
-
if (frame.model) setCurrentModel(frame.model)
|
|
566
|
-
}
|
|
567
|
-
if (frame.type === 'history_incomplete') {
|
|
568
|
-
setStatus('历史不完整:' + (frame.message || '回放被中断'))
|
|
569
|
-
setHistoryLoading(false)
|
|
570
|
-
}
|
|
571
|
-
if (frame.type === 'model_switched') {
|
|
572
|
-
if (frame.data?.model) setCurrentModel(frame.data.model)
|
|
573
|
-
setSwitchingModel(false)
|
|
574
|
-
setStatus('模型已切换')
|
|
613
|
+
let retries = 0
|
|
614
|
+
while (streamGenerationRef.current === generation) {
|
|
615
|
+
let stream
|
|
616
|
+
try {
|
|
617
|
+
stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd })
|
|
618
|
+
if (streamGenerationRef.current !== generation) {
|
|
619
|
+
stream.dispose()
|
|
620
|
+
return
|
|
575
621
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
if (frame.type === 'notification' && frame.method === '_monkeyai/session/state') {
|
|
590
|
-
const status = frame.data?.state?.status
|
|
591
|
-
if (typeof status === 'string' && SESSION_STATUS_LABEL[status]) {
|
|
592
|
-
setSessionStatus(status)
|
|
593
|
-
setTurnActive(status === 'running')
|
|
594
|
-
if (status !== 'running') setStopping(false)
|
|
622
|
+
streamRef.current = stream
|
|
623
|
+
setStatus(retries > 0 ? '远程会话已重新连接' : '远程会话已连接')
|
|
624
|
+
for await (const frame of stream) {
|
|
625
|
+
setFrames(current => [...current, frame])
|
|
626
|
+
if (frame.type === 'ready' && !historyLoadedRef.current) setStatus('正在回放会话历史')
|
|
627
|
+
if (frame.type === 'reconnecting') setStatus(frame.message || '正在重连远程 runtime')
|
|
628
|
+
if (frame.type === 'history_loaded') {
|
|
629
|
+
historyLoadedRef.current = true
|
|
630
|
+
setStatus('历史已加载')
|
|
631
|
+
setHistoryLoading(false)
|
|
632
|
+
setSessionStatus(current => current || 'idle')
|
|
633
|
+
if (frame.model) setCurrentModel(frame.model)
|
|
595
634
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
635
|
+
if (frame.type === 'history_incomplete') {
|
|
636
|
+
setStatus('历史不完整:' + (frame.message || '回放被中断'))
|
|
637
|
+
setHistoryLoading(false)
|
|
638
|
+
}
|
|
639
|
+
if (frame.type === 'model_switched') {
|
|
640
|
+
if (frame.data?.model) setCurrentModel(frame.data.model)
|
|
641
|
+
setSwitchingModel(false)
|
|
642
|
+
setStatus('模型已切换')
|
|
643
|
+
}
|
|
644
|
+
if (frame.type === 'model_switch_failed') {
|
|
645
|
+
setSwitchingModel(false)
|
|
646
|
+
setError(frame.message || '模型切换失败')
|
|
647
|
+
setStatus('模型切换失败')
|
|
648
|
+
}
|
|
649
|
+
if (frame.type === 'session_missing') {
|
|
650
|
+
setError(frame.message || '该会话在所选设备上已不存在。')
|
|
651
|
+
setStatus('设备上找不到该会话')
|
|
652
|
+
setHistoryLoading(false)
|
|
653
|
+
}
|
|
654
|
+
if (frame.type === 'request') {
|
|
655
|
+
setPendingRequests(current => [...current.filter(item => item.requestId !== frame.requestId), frame])
|
|
656
|
+
}
|
|
657
|
+
if (frame.type === 'notification' && frame.method === '_monkeyai/session/state') {
|
|
658
|
+
const status = frame.data?.state?.status
|
|
659
|
+
if (typeof status === 'string' && SESSION_STATUS_LABEL[status]) {
|
|
660
|
+
setSessionStatus(status)
|
|
661
|
+
setTurnActive(status === 'running')
|
|
662
|
+
if (status !== 'running') setStopping(false)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (frame.type === 'delivery') {
|
|
666
|
+
const accepted = Boolean(frame.data?.accepted)
|
|
667
|
+
setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type })
|
|
668
|
+
setTurnActive(false)
|
|
669
|
+
setStopping(false)
|
|
670
|
+
if (accepted) {
|
|
671
|
+
const cancelled = frame.data?.result?.stopReason === 'cancelled'
|
|
672
|
+
setStatus(cancelled ? '回合已停止' : '回合已完成')
|
|
673
|
+
} else {
|
|
674
|
+
setPrompt(current => current === '' ? (lastDraftRef.current || '') : current)
|
|
675
|
+
setStatus('MonkeyAI 未接受该指令,草稿已保留')
|
|
676
|
+
if (frame.message) setError(frame.message)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (frame.type === 'cancel_sent') {
|
|
680
|
+
setStopping(false)
|
|
681
|
+
setStatus('已请求停止,等待 runtime 取消')
|
|
609
682
|
}
|
|
683
|
+
if (frame.type === 'response_sent') {
|
|
684
|
+
setPendingRequests(current => current.filter(item => item.requestId !== frame.requestId))
|
|
685
|
+
}
|
|
686
|
+
if (frame.type === 'response_error') {
|
|
687
|
+
setError(`远程回复被拒绝:${frame.code || '未知'}`)
|
|
688
|
+
}
|
|
689
|
+
if (frame.type === 'error') setError(frame.message || frame.code || '远程会话错误')
|
|
610
690
|
}
|
|
611
|
-
if (
|
|
691
|
+
if (streamGenerationRef.current !== generation || streamRef.current !== stream) return
|
|
692
|
+
streamRef.current = null
|
|
693
|
+
setStatus('远程会话已断开')
|
|
694
|
+
setHistoryLoading(false)
|
|
695
|
+
setTurnActive(false)
|
|
696
|
+
setStopping(false)
|
|
697
|
+
return
|
|
698
|
+
} catch (reason) {
|
|
699
|
+
if (streamGenerationRef.current !== generation || (stream && streamRef.current !== stream)) return
|
|
700
|
+
if (isRemoteStreamCarrierError(reason)) {
|
|
701
|
+
retries += 1
|
|
702
|
+
try { stream?.dispose() } catch {}
|
|
703
|
+
streamRef.current = null
|
|
704
|
+
setError('')
|
|
705
|
+
setTurnActive(false)
|
|
612
706
|
setStopping(false)
|
|
613
|
-
setStatus('
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
if (frame.type === 'response_error') {
|
|
619
|
-
setError(`远程回复被拒绝:${frame.code || '未知'}`)
|
|
707
|
+
setStatus('远程连接中断,正在重连')
|
|
708
|
+
const delay = Math.min(1000 * (2 ** Math.min(retries - 1, 5)), 30_000)
|
|
709
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
710
|
+
continue
|
|
620
711
|
}
|
|
621
|
-
|
|
712
|
+
streamRef.current = null
|
|
713
|
+
setStatus('远程会话不可用')
|
|
714
|
+
setError(errorText(reason))
|
|
715
|
+
setHistoryLoading(false)
|
|
716
|
+
setTurnActive(false)
|
|
717
|
+
setStopping(false)
|
|
718
|
+
return
|
|
622
719
|
}
|
|
623
|
-
if (streamRef.current !== stream) return
|
|
624
|
-
setStatus('远程会话已断开')
|
|
625
|
-
setHistoryLoading(false)
|
|
626
|
-
setTurnActive(false)
|
|
627
|
-
setStopping(false)
|
|
628
|
-
} catch (reason) {
|
|
629
|
-
if (stream && streamRef.current !== stream) return
|
|
630
|
-
setStatus('远程会话不可用')
|
|
631
|
-
setError(errorText(reason))
|
|
632
|
-
setHistoryLoading(false)
|
|
633
|
-
setTurnActive(false)
|
|
634
|
-
setStopping(false)
|
|
635
720
|
}
|
|
636
721
|
}
|
|
637
722
|
|
|
@@ -853,9 +938,11 @@ function RemoteSessionPage({ api }) {
|
|
|
853
938
|
return h('main', { style: style.page, className: 'mai-panel' },
|
|
854
939
|
h('style', { dangerouslySetInnerHTML: { __html: PANEL_CSS } }),
|
|
855
940
|
h('header', { style: style.header },
|
|
856
|
-
h('div', { style: { display: '
|
|
857
|
-
h(
|
|
858
|
-
h('
|
|
941
|
+
h('div', { style: { display: 'grid', gap: 1 } },
|
|
942
|
+
h('p', { style: style.eyebrow }, '远程工作区'),
|
|
943
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
944
|
+
h(MonkeyIcon, { size: 22 }),
|
|
945
|
+
h('h1', { style: style.title, className: 'mai-page-title' }, 'MonkeyAI 会话'))),
|
|
859
946
|
h(StatusBadge, { status, live: isLive })),
|
|
860
947
|
error ? h('div', { role: 'alert', style: style.error },
|
|
861
948
|
h('span', { style: { flex: 1 } }, error),
|
|
@@ -939,11 +1026,13 @@ function RemoteSessionPage({ api }) {
|
|
|
939
1026
|
h('p', { style: { margin: 0, fontSize: 12, color: t.text3 } }, '浏览器授权需要回调能访问到本机 Host。手机上请改用上方账号登录。'))),
|
|
940
1027
|
accounts.length > 0 ? h('p', { style: { margin: 0, fontSize: 12, color: t.text3 } }, '已连接的账号会出现在设备列表上方的账号选择器中。') : null))
|
|
941
1028
|
: null,
|
|
942
|
-
currentAccount ? h('section', { style: style.
|
|
943
|
-
h('div', { style: style.row },
|
|
944
|
-
h('
|
|
945
|
-
|
|
946
|
-
|
|
1029
|
+
currentAccount ? h('section', { style: style.toolbarCard, className: 'mai-toolbar' },
|
|
1030
|
+
h('div', { style: style.row, className: 'mai-toolbar-heading' },
|
|
1031
|
+
h('div', { style: { display: 'grid', gap: 1, flex: 1, minWidth: 0 } },
|
|
1032
|
+
h('h2', { style: style.sectionTitle }, '远程会话'),
|
|
1033
|
+
h('p', { style: style.subtitle }, '选择设备,继续处理 MonkeyAI 会话')),
|
|
1034
|
+
h('span', { style: style.chip }, currentAccount.scopes.filter(s => s !== 'device:connect').join(' · '))),
|
|
1035
|
+
h('div', { style: style.row, className: 'mai-toolbar-controls' },
|
|
947
1036
|
h('select', { style: { ...style.input, flex: '1 1 200px' }, value: accountId, onChange: event => setAccountId(event.target.value), 'aria-label': '已授权账号' },
|
|
948
1037
|
accounts.map(account => h('option', { key: account.accountId, value: account.accountId }, account.serverOrigin))),
|
|
949
1038
|
h('button', { type: 'button', style: { ...style.button, flex: '0 0 auto', padding: '6px 10px', fontWeight: 700 }, disabled: loading || addingServer, onClick: () => { setAddingServer(true); setServerAddress(''); setAuthorizationRequestId(''); setAuthorizationUrl(''); setAuthorizationMode(''); setCallbackUrl(''); setStatus('请输入 MonkeyAI 服务器地址') }, title: '授权另一个 MonkeyAI 服务器' }, '+ 添加服务器'),
|
|
@@ -1005,30 +1094,41 @@ function RemoteSessionPage({ api }) {
|
|
|
1005
1094
|
type: 'button', style: style.primaryButton, flex: '0 0 auto',
|
|
1006
1095
|
disabled: loading || !machineId || !newSessionCwd.trim() || newSessionBusy,
|
|
1007
1096
|
onClick: createRemoteSession,
|
|
1008
|
-
}, newSessionBusy ? '创建中…' : '创建'))))
|
|
1009
|
-
sessionOptions.length > 0
|
|
1010
|
-
? h('div', { style: style.sessions }, sessionOptions.map(session => {
|
|
1011
|
-
const flag = session.stale ? '已过期' : session.deviceStatus && session.deviceStatus !== 'online' ? (session.deviceStatus === 'offline' ? '离线' : session.deviceStatus) : ''
|
|
1012
|
-
return h('button', {
|
|
1013
|
-
style: { ...style.session, ...(session.id === sessionId ? style.selected : {}) },
|
|
1014
|
-
className: 'mai-card-hover',
|
|
1015
|
-
key: session.id,
|
|
1016
|
-
type: 'button',
|
|
1017
|
-
disabled: !session.cwd,
|
|
1018
|
-
onClick: () => openSession(session),
|
|
1019
|
-
title: session.cwd ? `打开 ${session.cwd}` : 'ACP 会话列表未提供工作目录',
|
|
1020
|
-
},
|
|
1021
|
-
h('span', { style: style.sessionTitle }, session.title || session.id),
|
|
1022
|
-
h('span', { style: style.sessionMeta }, session.cwd || '加载历史需要 runtime 提供工作目录'),
|
|
1023
|
-
flag !== '' ? h('span', { style: { fontSize: 10.5, fontWeight: 650, color: session.stale ? t.text3 : '#8a6d1d', textTransform: 'uppercase', letterSpacing: '0.04em' } }, flag) : null)
|
|
1024
|
-
}))
|
|
1025
|
-
: h('p', { style: style.subtitle }, machineId ? '该设备暂无会话。可在上方新建,或点击刷新。' : '请选择设备以列出其会话。'))
|
|
1097
|
+
}, newSessionBusy ? '创建中…' : '创建')))))
|
|
1026
1098
|
: null,
|
|
1027
|
-
|
|
1028
|
-
h('
|
|
1099
|
+
currentAccount ? h('div', { className: 'mai-workspace' },
|
|
1100
|
+
h('aside', { style: style.sessionSidebar, className: 'mai-session-sidebar' },
|
|
1101
|
+
h('div', { style: { ...style.row, justifyContent: 'space-between' } },
|
|
1102
|
+
h('h2', { style: style.sectionTitle }, '会话列表'),
|
|
1103
|
+
h('span', { style: style.chip }, String(sessionOptions.length))),
|
|
1104
|
+
sessionOptions.length > 0
|
|
1105
|
+
? h('div', { style: style.sessions, className: 'mai-session-list' }, sessionOptions.map(session => {
|
|
1106
|
+
const flag = session.stale ? '已过期' : session.deviceStatus && session.deviceStatus !== 'online' ? (session.deviceStatus === 'offline' ? '离线' : session.deviceStatus) : ''
|
|
1107
|
+
return h('button', {
|
|
1108
|
+
style: { ...style.session, ...(session.id === sessionId ? style.selected : {}) },
|
|
1109
|
+
className: 'mai-card-hover mai-session-card',
|
|
1110
|
+
'aria-current': session.id === sessionId ? 'true' : undefined,
|
|
1111
|
+
key: session.id,
|
|
1112
|
+
type: 'button',
|
|
1113
|
+
disabled: !session.cwd,
|
|
1114
|
+
onClick: () => openSession(session),
|
|
1115
|
+
title: session.cwd ? `打开 ${session.cwd}` : 'ACP 会话列表未提供工作目录',
|
|
1116
|
+
},
|
|
1117
|
+
h('span', { style: style.sessionTitle }, session.title || session.id),
|
|
1118
|
+
h('span', { style: style.sessionMeta, className: 'mai-session-meta' }, session.cwd || '加载历史需要 runtime 提供工作目录'),
|
|
1119
|
+
flag !== '' ? h('span', { style: { fontSize: 10.5, fontWeight: 650, color: session.stale ? t.text3 : '#8a6d1d', textTransform: 'uppercase', letterSpacing: '0.04em' } }, flag) : null)
|
|
1120
|
+
}))
|
|
1121
|
+
: h('div', { style: { display: 'grid', justifyItems: 'center', gap: 7, padding: '22px 12px', textAlign: 'center' } },
|
|
1122
|
+
h('span', { style: { ...style.avatar, width: 34, height: 34, background: t.surfaceAlt, color: t.text3, fontSize: 17 } }, '·'),
|
|
1123
|
+
h('span', { style: { fontSize: 12, fontWeight: 600, color: t.text2 } }, machineId ? '这里还没有会话' : '请选择远程设备'),
|
|
1124
|
+
h('span', { style: { fontSize: 11, color: t.text3 } }, machineId ? '刷新会话或创建一个新会话' : '会话列表会显示在这里')),
|
|
1125
|
+
h('div', { style: { borderTop: `1px solid ${t.border}`, paddingTop: 8 } },
|
|
1126
|
+
h('p', { style: { margin: 0, fontSize: 10.5, color: t.text3 } }, machineId ? '会话来自所选设备的 runtime' : '连接设备后查看会话'))),
|
|
1127
|
+
sessionId ? h('section', { style: { ...style.card, ...style.thread }, className: 'mai-thread' },
|
|
1128
|
+
h('div', { style: style.row, className: 'mai-thread-header' },
|
|
1029
1129
|
h('button', {
|
|
1030
1130
|
type: 'button', style: { ...style.button, flexShrink: 0 }, title: '关闭会话并返回列表',
|
|
1031
|
-
onClick: () => { streamRef.current?.dispose(); streamRef.current = null; setSessionId(''); setTurnActive(false); setStopping(false); setSessionStatus(''); setStatus('远程会话已关闭') },
|
|
1131
|
+
onClick: () => { streamGenerationRef.current += 1; streamRef.current?.dispose(); streamRef.current = null; setSessionId(''); setTurnActive(false); setStopping(false); setSessionStatus(''); setStatus('远程会话已关闭') },
|
|
1032
1132
|
}, '←'),
|
|
1033
1133
|
h('strong', { style: { flex: 1, fontSize: 13.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title }, title),
|
|
1034
1134
|
sessionStatus ? h('span', {
|
|
@@ -1038,7 +1138,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1038
1138
|
borderColor: sessionStatus === 'running' ? t.accent : sessionStatus === 'error' ? t.dangerBorder : t.border,
|
|
1039
1139
|
color: sessionStatus === 'running' ? t.accent : sessionStatus === 'error' ? t.danger : t.text2,
|
|
1040
1140
|
},
|
|
1041
|
-
title: '
|
|
1141
|
+
title: '会话状态',
|
|
1042
1142
|
'aria-label': `会话状态:${SESSION_STATUS_LABEL[sessionStatus]}`,
|
|
1043
1143
|
},
|
|
1044
1144
|
sessionStatus === 'running' ? h('span', { className: 'mai-pulse', style: { ...style.dot, background: t.accent } }) : null,
|
|
@@ -1057,7 +1157,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1057
1157
|
? h('option', { value: currentModel }, currentModel.slice(0, 24))
|
|
1058
1158
|
: null,
|
|
1059
1159
|
models.map(m => h('option', { key: m.id, value: m.id }, `${m.displayName} (${Math.round(m.contextWindowTokens / 1000)}k)`)))),
|
|
1060
|
-
h('div', { style: style.events, 'aria-live': 'polite', 'data-events': '', ref: eventsRef,
|
|
1160
|
+
h('div', { style: style.events, className: 'mai-thread-events mai-events', 'aria-live': 'polite', 'data-events': '', ref: eventsRef,
|
|
1061
1161
|
onScroll: event => {
|
|
1062
1162
|
const el = event.currentTarget
|
|
1063
1163
|
if (el.scrollTop < 60 && visibleCount < displayEntries.length) {
|
|
@@ -1160,8 +1260,13 @@ function RemoteSessionPage({ api }) {
|
|
|
1160
1260
|
h('path', { d: 'M8 12.5V3.5M8 3.5L4 7.5M8 3.5L12 7.5', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' })))),
|
|
1161
1261
|
lastDelivery || turnActive ? h('div', { style: { fontSize: 11.5, color: turnActive ? t.accent : lastDelivery?.accepted ? t.success : lastDelivery?.frame === 'pending' ? t.text3 : t.danger, textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 } },
|
|
1162
1262
|
turnActive ? h('span', { className: 'mai-pulse', style: { ...style.dot, background: t.accent } }) : null,
|
|
1163
|
-
turnActive ? (stopping ? '正在停止当前回合…' :
|
|
1164
|
-
:
|
|
1263
|
+
turnActive ? (stopping ? '正在停止当前回合…' : 'MonkeyAI 正在处理…') : lastDelivery?.accepted ? '回合已结束' : lastDelivery?.frame === 'pending' ? '等待投递' : '投递被拒绝') : null)
|
|
1264
|
+
: h('section', { style: style.emptyThread, className: 'mai-empty-thread' },
|
|
1265
|
+
h('div', { className: 'mai-empty-mark' }, h(MonkeyIcon, { size: 26 })),
|
|
1266
|
+
h('p', { style: style.eyebrow }, '远程会话'),
|
|
1267
|
+
h('h2', { style: { margin: 0, fontSize: 18, fontWeight: 650, color: '#182230' } }, '选择一个会话'),
|
|
1268
|
+
h('p', { style: { ...style.subtitle, maxWidth: 300 } }, sessionOptions.length > 0 ? '从左侧选择会话,在此继续对话。' : machineId ? '当前设备还没有会话。可在上方新建或刷新。' : '先选择一个远程设备。'))
|
|
1269
|
+
) : null)
|
|
1165
1270
|
}
|
|
1166
1271
|
|
|
1167
1272
|
function MonkeyIcon({ size = 18 }) {
|