fix: session-cwd attach race, terminal failure surfacing, white-screen hardening; CodeMirror editor
- cwd resolution falls back (attached header → client summary cwd → process cwd) across the JSON API, media route and terminal upgrade, so explorer/ git/terminal work from first paint instead of 'session ... has no working directory' while the web client hydrates the session (refresh-only fix). - terminal client threads the summary cwd into the WS URL and stops the reconnect loop after three unreasoned failures, showing the close code with a manual retry; a 1011+reason refusal already showed the reason. - white screen: persisted state is structurally sanitized on load (malformed shapes fall back to default) and the restored width is clamped to the viewport; the layout margin is capped at the viewport; the client entry wraps every lifecycle step in try/catch and pins a visible diagnostic bar on load/mount/interception failure instead of failing silently. - editor: textarea replaced with CodeMirror 6 — line wrapping, per-extension syntax highlighting (js/ts/jsx/tsx, json, python, html, css, xml, yaml, sql, java, c/c++, rust, go, php, shell, toml, nginx, dockerfile, properties), one-dark theme, dirty dot + Ctrl/Cmd+S save kept. - tests: +9 (cwd fallback chain over the API route, state sanitization, language mapping); 36 pass, typecheck + build + vm materialize clean.
This commit is contained in:
@@ -5,17 +5,22 @@
|
||||
* type:"resize". Transient disconnects (page refresh, host restart) reconnect
|
||||
* automatically; a server-side refusal (close code 1011 with a reason, e.g.
|
||||
* a failed pty spawn) stops the loop and shows the reason with a manual
|
||||
* retry, so the banner never spins forever.
|
||||
* retry, and repeated unreasoned failures surface the close code after three
|
||||
* attempts, so the banner never spins forever.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import 'xterm/css/xterm.css'
|
||||
import { t } from './locales.ts'
|
||||
import type { SessionScope } from './api.ts'
|
||||
import css from './sidebar.module.css'
|
||||
|
||||
export function TerminalView(props: { sessionId: string; tabId: string }) {
|
||||
const { sessionId, tabId } = props
|
||||
/** How many consecutive unreasoned failures before showing the error banner. */
|
||||
const FAILURE_LIMIT = 3
|
||||
|
||||
export function TerminalView(props: { scope: SessionScope; tabId: string }) {
|
||||
const { scope, tabId } = props
|
||||
const hostRef = useRef<HTMLDivElement>(null)
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [fatal, setFatal] = useState<string | null>(null)
|
||||
@@ -40,10 +45,12 @@ export function TerminalView(props: { sessionId: string; tabId: string }) {
|
||||
let socket: WebSocket | null = null
|
||||
let closed = false
|
||||
let retry: number | undefined
|
||||
let failures = 0
|
||||
|
||||
const wsUrl = (): string => {
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const params = new URLSearchParams({ sessionId, tab: tabId })
|
||||
const params = new URLSearchParams({ sessionId: scope.sessionId, tab: tabId })
|
||||
if (scope.cwd !== undefined && scope.cwd !== '') params.set('cwd', scope.cwd)
|
||||
return `${proto}//${location.host}/sidebar/ws/terminal?${params.toString()}`
|
||||
}
|
||||
|
||||
@@ -57,6 +64,7 @@ export function TerminalView(props: { sessionId: string; tabId: string }) {
|
||||
if (closed) return
|
||||
socket = new WebSocket(wsUrl())
|
||||
socket.onopen = () => {
|
||||
failures = 0
|
||||
setConnected(true)
|
||||
setFatal(null)
|
||||
sendResize()
|
||||
@@ -72,6 +80,15 @@ export function TerminalView(props: { sessionId: string; tabId: string }) {
|
||||
setFatal(event.reason)
|
||||
return
|
||||
}
|
||||
// Unreasoned drops (upgrade rejected, host down, mid-handshake
|
||||
// refusal) normally recover on the next attempt; after a few
|
||||
// consecutive failures stop spinning and show the close code.
|
||||
failures += 1
|
||||
if (failures >= FAILURE_LIMIT) {
|
||||
const detail = event.reason !== '' ? ` (${event.code}: ${event.reason})` : ` (${event.code})`
|
||||
setFatal(`${t('terminalConnectFailed')}${detail}`)
|
||||
return
|
||||
}
|
||||
if (!closed) retry = window.setTimeout(connect, 2000)
|
||||
}
|
||||
socket.onerror = () => {
|
||||
@@ -109,7 +126,7 @@ export function TerminalView(props: { sessionId: string; tabId: string }) {
|
||||
term.dispose()
|
||||
connectRef.current = null
|
||||
}
|
||||
}, [sessionId, tabId])
|
||||
}, [scope.sessionId, scope.cwd, tabId])
|
||||
|
||||
return (
|
||||
<div className={css.terminalWrap}>
|
||||
|
||||
Reference in New Issue
Block a user