Skip to content

Commit 0a9ca2a

Browse files
deepagent-aiclaude
andcommitted
fix(app): 恢复的终端切回项目后立即显示,不再等待 WebSocket 握手
restore() 恢复的 PTY 标记 restored:true,Terminal 组件在 xterm 初始化完成后立即 markReady(),隐藏'连接中'遮罩;WebSocket 仍在后台 建立,连接前键入的内容缓冲在 inputBuffer,socket 就绪后一次性 flush。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent aebad4a commit 0a9ca2a

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

packages/app/src/components/terminal.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export interface TerminalProps extends ComponentProps<"div"> {
3535
runtimeId?: string
3636
onSubmit?: () => void
3737
onStatusChange?: (status: TerminalStatus, error?: TerminalFailure) => void
38+
/** When true (restored PTY), show the terminal immediately after xterm initialises
39+
* instead of waiting for the WebSocket handshake. The WebSocket still connects in
40+
* the background; input typed before it's ready is buffered. */
41+
optimisticReady?: boolean
3842
}
3943

4044
let shared: Promise<{ mod: typeof import("ghostty-web"); ghostty: Ghostty }> | undefined
@@ -159,6 +163,7 @@ export const Terminal = (props: TerminalProps) => {
159163
"runtimeId",
160164
"onSubmit",
161165
"onStatusChange",
166+
"optimisticReady",
162167
])
163168
const id = local.pty.ptyId
164169
let ws: WebSocket | undefined
@@ -435,6 +440,8 @@ export const Terminal = (props: TerminalProps) => {
435440
})
436441
cleanups.push(() => disposeIfDisposable(onResize))
437442
const onData = t.onData((data) => {
443+
// When optimisticReady is active the buffering handler below takes over.
444+
if (local.optimisticReady) return
438445
if (ws?.readyState === WebSocket.OPEN) ws.send(data)
439446
})
440447
cleanups.push(() => disposeIfDisposable(onData))
@@ -456,6 +463,28 @@ export const Terminal = (props: TerminalProps) => {
456463
scheduleSize(t.cols, t.rows)
457464
startResize()
458465

466+
// For restored PTYs: show the terminal surface immediately after xterm is
467+
// initialised instead of waiting for the full WebSocket handshake (which can
468+
// take 2-4 s). Input typed before the socket opens is buffered and flushed
469+
// once the connection is established.
470+
let inputBuffer = local.optimisticReady ? "" : undefined
471+
if (local.optimisticReady) {
472+
markReady()
473+
// Intercept onData to buffer keystrokes until the WebSocket is open.
474+
const onDataOpt = t.onData((data) => {
475+
if (ws?.readyState === WebSocket.OPEN) {
476+
if (inputBuffer) {
477+
ws.send(inputBuffer)
478+
inputBuffer = undefined
479+
}
480+
ws.send(data)
481+
} else {
482+
inputBuffer = (inputBuffer ?? "") + data
483+
}
484+
})
485+
cleanups.push(() => disposeIfDisposable(onDataOpt))
486+
}
487+
459488
const once = { value: false }
460489
const decoder = new TextDecoder()
461490

packages/app/src/context/terminal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export type LocalPTY = {
5252
titleNumber: number
5353
status: TerminalStatus
5454
error?: TerminalFailure
55+
/** True when restored from cross-project navigation cache; cleared on first ready. */
56+
restored?: boolean
5557
}
5658

5759
export type TerminalStore = {
@@ -567,6 +569,7 @@ function createWorkspaceTerminalSession(
567569
titleNumber: p.titleNumber,
568570
status: "connecting" as TerminalStatus,
569571
error: undefined,
572+
restored: true,
570573
})),
571574
)
572575
setRootSignal(clonePaneTree(snapshot.root))
@@ -671,7 +674,7 @@ function createWorkspaceTerminalSession(
671674
setStatus(id: string, ptyId: string, status: TerminalStatus, error?: TerminalFailure) {
672675
const index = store.all.findIndex((pty) => pty.id === id && pty.ptyId === ptyId)
673676
if (index === -1) return
674-
setStore("all", index, { status, error: status === "ready" ? undefined : error })
677+
setStore("all", index, { status, error: status === "ready" ? undefined : error, ...(status === "ready" ? { restored: false } : {}) })
675678
},
676679
update(input: Partial<LocalPTY> & { id: string }) {
677680
if (input.title === undefined) return

packages/app/src/pages/session/terminal-view.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ function TerminalSessionView(props: { pty: LocalPTY; focused: boolean }) {
309309
autoFocus={props.focused}
310310
runtimeId={terminal.runtimeId()}
311311
onStatusChange={(next, error) => terminal.setStatus(props.pty.id, ptyId, next, error)}
312+
optimisticReady={props.pty.restored}
312313
/>
313314
)}
314315
</Show>

0 commit comments

Comments
 (0)