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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-monkeyai-sessions",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "在 DeepSeek Harness 中读取和继续 MonkeyAI 会话:查看完整对话历史、下发新指令,无缝接续之前在 MonkeyAI 客户端上的工作",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,12 +28,19 @@ const t = {
28
28
  }
29
29
 
30
30
  const style = {
31
- page: { display: 'grid', gap: 12, maxWidth: 1080, margin: '0 auto', padding: '16px 18px 24px', color: t.text, height: '100%', minHeight: 0, overflowY: 'auto', boxSizing: 'border-box', alignContent: 'start', fontSize: 13, lineHeight: 1.5 },
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: 15, fontWeight: 650, letterSpacing: '-0.01em', color: t.text },
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: t.radiusLg, background: t.surface },
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: 8, height: '56vh', minHeight: 200, overflowY: 'auto', padding: '4px 6px 4px 2px' },
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: t.radiusLg, padding: '8px 13px', background: t.surfaceAlt, border: `1px solid ${t.border}`, lineHeight: 1.55, fontSize: 13 },
61
- bubbleUser: { maxWidth: '88%', minWidth: 0, flex: '0 1 auto', width: 'fit-content', borderRadius: t.radiusLg, padding: '8px 13px', background: t.accent, color: '#ffffff', lineHeight: 1.55, fontSize: 13 },
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
- const results = await Promise.allSettled([
521
- unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 })),
522
- unwrap(api.listRuntimeSessions({ accountId, machineId: mid })),
523
- ])
524
- const errors = []
525
- if (results[0].status === 'fulfilled') setSummarySessions(results[0].value.items)
526
- else errors.push(`会话摘要索引:${errorText(results[0].reason)}`)
527
- if (results[1].status === 'fulfilled') setRuntimeSessions(results[1].value.sessions)
528
- else errors.push(`ACP 会话列表:${errorText(results[1].reason)}`)
529
- setStatus(errors.length === 0 ? '会话已刷新' : '部分会话来源不可用')
530
- setError(errors.join('\n'))
531
- setLoading(false)
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 stream
552
- try {
553
- stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd })
554
- streamRef.current = stream
555
- setStatus('远程会话已连接')
556
- for await (const frame of stream) {
557
- setFrames(current => [...current, frame])
558
- if (frame.type === 'ready' && !historyLoadedRef.current) setStatus('正在回放会话历史')
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
- if (frame.type === 'model_switch_failed') {
577
- setSwitchingModel(false)
578
- setError(frame.message || '模型切换被拒绝')
579
- setStatus('模型切换失败')
580
- }
581
- if (frame.type === 'session_missing') {
582
- setError(frame.message || '该会话在所选设备上已不存在。')
583
- setStatus('设备上找不到该会话')
584
- setHistoryLoading(false)
585
- }
586
- if (frame.type === 'request') {
587
- setPendingRequests(current => [...current.filter(item => item.requestId !== frame.requestId), frame])
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
- if (frame.type === 'delivery') {
598
- const accepted = Boolean(frame.data?.accepted)
599
- setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type })
600
- setTurnActive(false)
601
- setStopping(false)
602
- if (accepted) {
603
- const cancelled = frame.data?.result?.stopReason === 'cancelled'
604
- setStatus(cancelled ? '回合已停止' : '回合已完成')
605
- } else {
606
- setPrompt(current => current === '' ? (lastDraftRef.current || '') : current)
607
- setStatus('MonkeyAI 未接受该指令,草稿已保留')
608
- if (frame.message) setError(frame.message)
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 (frame.type === 'cancel_sent') {
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('已请求停止,等待 runtime 取消')
614
- }
615
- if (frame.type === 'response_sent') {
616
- setPendingRequests(current => current.filter(item => item.requestId !== frame.requestId))
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
- if (frame.type === 'error') setError(frame.message || frame.code || '远程会话错误')
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: 'flex', alignItems: 'center', gap: 8 } },
857
- h(MonkeyIcon, { size: 22 }),
858
- h('h1', { style: style.title }, 'MonkeyAI 会话')),
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.card },
943
- h('div', { style: style.row },
944
- h('h2', { style: { ...style.sectionTitle, flex: 1 } }, '远程会话'),
945
- h('span', { style: { fontSize: 11, color: t.text3 } }, currentAccount.scopes.filter(s => s !== 'device:connect').join(' · '))),
946
- h('div', { style: style.row },
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
- sessionId ? h('section', { style: style.card },
1028
- h('div', { style: style.row },
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 ? '正在停止当前回合…' : sessionStatus === 'running' ? 'MonkeyAI 正在处理(远端客户端发起)…' : 'MonkeyAI 正在处理…') : lastDelivery?.accepted ? '回合已结束' : lastDelivery?.frame === 'pending' ? '等待投递' : '投递被拒绝') : null)
1164
- : null)
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 }) {