Feedback sweep: spinners on every async button, toasts for action outcomes, top activity bar, loading states on every page, terminal connecting overlay, dashboard save indicator, task pane pinned above the package list with auto-scroll and completion toast
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d08b246b5f
commit
724f30c02b
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import { wsURL } from './api'
|
||||
import { Spinner } from './ui'
|
||||
|
||||
export interface TermHandle { input: (s: string) => void; armCtrl: (v: boolean) => void }
|
||||
|
||||
@@ -28,6 +29,7 @@ export default function TerminalPane({ id, active, register, onStatus, onExit }:
|
||||
const cbs = useRef({ onStatus, onExit })
|
||||
cbs.current = { onStatus, onExit }
|
||||
const fitRef = useRef<() => void>(() => {})
|
||||
const [conn, setConn] = useState<'connecting' | 'open' | 'reconnecting' | 'exited'>('connecting')
|
||||
|
||||
useEffect(() => {
|
||||
const el = box.current!
|
||||
@@ -67,12 +69,12 @@ export default function TerminalPane({ id, active, register, onStatus, onExit }:
|
||||
const sock = new WebSocket(wsURL(id))
|
||||
sock.binaryType = 'arraybuffer'
|
||||
ws = sock
|
||||
sock.onopen = () => { retry = 0; cbs.current.onStatus(true); lastSize = ''; doFit() }
|
||||
sock.onopen = () => { retry = 0; setConn('open'); cbs.current.onStatus(true); lastSize = ''; doFit() }
|
||||
sock.onmessage = (ev) => {
|
||||
if (ev.data instanceof ArrayBuffer) { term.write(new Uint8Array(ev.data)); return }
|
||||
try {
|
||||
const m = JSON.parse(ev.data)
|
||||
if (m.type === 'exit') { term.write('\r\n\x1b[90m[process exited' + (m.code !== undefined ? ` (${m.code})` : '') + ']\x1b[0m\r\n'); closed = true; cbs.current.onExit() }
|
||||
if (m.type === 'exit') { term.write('\r\n\x1b[90m[process exited' + (m.code !== undefined ? ` (${m.code})` : '') + ']\x1b[0m\r\n'); closed = true; setConn('exited'); cbs.current.onExit() }
|
||||
if (m.type === 'closed') closed = true
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
@@ -80,6 +82,7 @@ export default function TerminalPane({ id, active, register, onStatus, onExit }:
|
||||
if (ws !== sock) return
|
||||
cbs.current.onStatus(false)
|
||||
if (closed) return
|
||||
setConn('reconnecting')
|
||||
retryTimer = window.setTimeout(connect, Math.min(1000 * 2 ** retry++, 15000))
|
||||
}
|
||||
sock.onerror = () => sock.close()
|
||||
@@ -124,5 +127,12 @@ export default function TerminalPane({ id, active, register, onStatus, onExit }:
|
||||
return () => clearTimeout(t)
|
||||
}, [active])
|
||||
|
||||
return <div ref={box} className="term" style={{ display: active ? 'block' : 'none' }} />
|
||||
return (
|
||||
<div className="term" style={{ display: active ? 'block' : 'none' }}>
|
||||
<div ref={box} className="term-inner" />
|
||||
{(conn === 'connecting' || conn === 'reconnecting') && (
|
||||
<div className="term-overlay"><Spinner /> {conn === 'connecting' ? 'connecting…' : 'reconnecting…'}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user