From 8aa1d13a81ba33fea567a1b36b6524c62624e05a Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sat, 28 Mar 2026 14:32:26 -0500 Subject: [PATCH] Handle terminal cwd errors with fallback and warning UI - fall back to the home directory when terminal cwd validation fails - surface plain error messages from the server - render terminal system messages as a warning line --- apps/server/src/terminal/Layers/Manager.ts | 31 +++++++++++++------ apps/server/src/wsServer.ts | 4 +++ .../src/components/ThreadTerminalDrawer.tsx | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index e9b57aef1..5b325d729 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -1,5 +1,6 @@ import { EventEmitter } from "node:events"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { @@ -536,7 +537,12 @@ export class TerminalManagerRuntime extends EventEmitter async open(raw: TerminalOpenInput): Promise { const input = decodeTerminalOpenInput(raw); return this.runWithThreadLock(input.threadId, async () => { - await this.assertValidCwd(input.cwd); + let cwd = input.cwd; + try { + await this.assertValidCwd(cwd); + } catch { + cwd = os.homedir(); + } const sessionKey = toSessionKey(input.threadId, input.terminalId); const existing = this.sessions.get(sessionKey); @@ -548,7 +554,7 @@ export class TerminalManagerRuntime extends EventEmitter const session: TerminalSessionState = { threadId: input.threadId, terminalId: input.terminalId, - cwd: input.cwd, + cwd, status: "starting", pid: null, history, @@ -566,7 +572,7 @@ export class TerminalManagerRuntime extends EventEmitter }; this.sessions.set(sessionKey, session); this.evictInactiveSessionsIfNeeded(); - await this.startSession(session, { ...input, cols, rows }, "started"); + await this.startSession(session, { ...input, cwd, cols, rows }, "started"); return this.snapshot(session); } @@ -577,9 +583,9 @@ export class TerminalManagerRuntime extends EventEmitter const runtimeEnvChanged = JSON.stringify(currentRuntimeEnv) !== JSON.stringify(nextRuntimeEnv); - if (existing.cwd !== input.cwd || runtimeEnvChanged) { + if (existing.cwd !== cwd || runtimeEnvChanged) { this.stopProcess(existing); - existing.cwd = input.cwd; + existing.cwd = cwd; existing.runtimeEnv = nextRuntimeEnv; existing.history = ""; existing.pendingHistoryControlSequence = ""; @@ -596,7 +602,7 @@ export class TerminalManagerRuntime extends EventEmitter if (!existing.process) { await this.startSession( existing, - { ...input, cols: targetCols, rows: targetRows }, + { ...input, cwd, cols: targetCols, rows: targetRows }, "started", ); return this.snapshot(existing); @@ -661,7 +667,12 @@ export class TerminalManagerRuntime extends EventEmitter async restart(raw: TerminalRestartInput): Promise { const input = decodeTerminalRestartInput(raw); return this.runWithThreadLock(input.threadId, async () => { - await this.assertValidCwd(input.cwd); + let cwd = input.cwd; + try { + await this.assertValidCwd(cwd); + } catch { + cwd = os.homedir(); + } const sessionKey = toSessionKey(input.threadId, input.terminalId); let session = this.sessions.get(sessionKey); @@ -671,7 +682,7 @@ export class TerminalManagerRuntime extends EventEmitter session = { threadId: input.threadId, terminalId: input.terminalId, - cwd: input.cwd, + cwd, status: "starting", pid: null, history: "", @@ -691,7 +702,7 @@ export class TerminalManagerRuntime extends EventEmitter this.evictInactiveSessionsIfNeeded(); } else { this.stopProcess(session); - session.cwd = input.cwd; + session.cwd = cwd; session.runtimeEnv = normalizedRuntimeEnv(input.env); } @@ -701,7 +712,7 @@ export class TerminalManagerRuntime extends EventEmitter session.history = ""; session.pendingHistoryControlSequence = ""; await this.persistHistory(input.threadId, input.terminalId, session.history); - await this.startSession(session, { ...input, cols, rows }, "restarted"); + await this.startSession(session, { ...input, cwd, cols, rows }, "restarted"); return this.snapshot(session); }); } diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 050768d32..7d93d6017 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -1202,6 +1202,10 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< }; } + if (squashed instanceof Error) { + return { message: squashed.message }; + } + return { message: Cause.pretty(cause) }; }; diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 07c183fd5..751deffbd 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -49,7 +49,7 @@ function clampDrawerHeight(height: number): number { } function writeSystemMessage(terminal: Terminal, message: string): void { - terminal.write(`\r\n[terminal] ${message}\r\n`); + terminal.write(`\r\n\x1b[33m⚠ ${message}\x1b[0m\r\n`); } function terminalThemeFromApp(): ITheme {