Skip to content

Commit 142ebf1

Browse files
authored
Handle terminal cwd errors with fallback and warning UI (#86)
- 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
1 parent cf82b60 commit 142ebf1

3 files changed

Lines changed: 26 additions & 11 deletions

File tree

apps/server/src/terminal/Layers/Manager.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EventEmitter } from "node:events";
22
import fs from "node:fs";
3+
import os from "node:os";
34
import path from "node:path";
45

56
import {
@@ -536,7 +537,12 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
536537
async open(raw: TerminalOpenInput): Promise<TerminalSessionSnapshot> {
537538
const input = decodeTerminalOpenInput(raw);
538539
return this.runWithThreadLock(input.threadId, async () => {
539-
await this.assertValidCwd(input.cwd);
540+
let cwd = input.cwd;
541+
try {
542+
await this.assertValidCwd(cwd);
543+
} catch {
544+
cwd = os.homedir();
545+
}
540546

541547
const sessionKey = toSessionKey(input.threadId, input.terminalId);
542548
const existing = this.sessions.get(sessionKey);
@@ -548,7 +554,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
548554
const session: TerminalSessionState = {
549555
threadId: input.threadId,
550556
terminalId: input.terminalId,
551-
cwd: input.cwd,
557+
cwd,
552558
status: "starting",
553559
pid: null,
554560
history,
@@ -566,7 +572,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
566572
};
567573
this.sessions.set(sessionKey, session);
568574
this.evictInactiveSessionsIfNeeded();
569-
await this.startSession(session, { ...input, cols, rows }, "started");
575+
await this.startSession(session, { ...input, cwd, cols, rows }, "started");
570576
return this.snapshot(session);
571577
}
572578

@@ -577,9 +583,9 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
577583
const runtimeEnvChanged =
578584
JSON.stringify(currentRuntimeEnv) !== JSON.stringify(nextRuntimeEnv);
579585

580-
if (existing.cwd !== input.cwd || runtimeEnvChanged) {
586+
if (existing.cwd !== cwd || runtimeEnvChanged) {
581587
this.stopProcess(existing);
582-
existing.cwd = input.cwd;
588+
existing.cwd = cwd;
583589
existing.runtimeEnv = nextRuntimeEnv;
584590
existing.history = "";
585591
existing.pendingHistoryControlSequence = "";
@@ -596,7 +602,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
596602
if (!existing.process) {
597603
await this.startSession(
598604
existing,
599-
{ ...input, cols: targetCols, rows: targetRows },
605+
{ ...input, cwd, cols: targetCols, rows: targetRows },
600606
"started",
601607
);
602608
return this.snapshot(existing);
@@ -661,7 +667,12 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
661667
async restart(raw: TerminalRestartInput): Promise<TerminalSessionSnapshot> {
662668
const input = decodeTerminalRestartInput(raw);
663669
return this.runWithThreadLock(input.threadId, async () => {
664-
await this.assertValidCwd(input.cwd);
670+
let cwd = input.cwd;
671+
try {
672+
await this.assertValidCwd(cwd);
673+
} catch {
674+
cwd = os.homedir();
675+
}
665676

666677
const sessionKey = toSessionKey(input.threadId, input.terminalId);
667678
let session = this.sessions.get(sessionKey);
@@ -671,7 +682,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
671682
session = {
672683
threadId: input.threadId,
673684
terminalId: input.terminalId,
674-
cwd: input.cwd,
685+
cwd,
675686
status: "starting",
676687
pid: null,
677688
history: "",
@@ -691,7 +702,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
691702
this.evictInactiveSessionsIfNeeded();
692703
} else {
693704
this.stopProcess(session);
694-
session.cwd = input.cwd;
705+
session.cwd = cwd;
695706
session.runtimeEnv = normalizedRuntimeEnv(input.env);
696707
}
697708

@@ -701,7 +712,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
701712
session.history = "";
702713
session.pendingHistoryControlSequence = "";
703714
await this.persistHistory(input.threadId, input.terminalId, session.history);
704-
await this.startSession(session, { ...input, cols, rows }, "restarted");
715+
await this.startSession(session, { ...input, cwd, cols, rows }, "restarted");
705716
return this.snapshot(session);
706717
});
707718
}

apps/server/src/wsServer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,10 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
12021202
};
12031203
}
12041204

1205+
if (squashed instanceof Error) {
1206+
return { message: squashed.message };
1207+
}
1208+
12051209
return { message: Cause.pretty(cause) };
12061210
};
12071211

apps/web/src/components/ThreadTerminalDrawer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function clampDrawerHeight(height: number): number {
4949
}
5050

5151
function writeSystemMessage(terminal: Terminal, message: string): void {
52-
terminal.write(`\r\n[terminal] ${message}\r\n`);
52+
terminal.write(`\r\n\x1b[33m⚠ ${message}\x1b[0m\r\n`);
5353
}
5454

5555
function terminalThemeFromApp(): ITheme {

0 commit comments

Comments
 (0)