Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 85 additions & 35 deletions src/components/terminal/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,47 +657,97 @@ export default class TerminalComponent {

const wsUrl = `ws://localhost:${this.options.port}/terminals/${pid}`;

this.websocket = new WebSocket(wsUrl);

this.websocket.onopen = () => {
this.isConnected = true;
this.onConnect?.();

// Load attach addon after connection
this.attachAddon = new AttachAddon(this.websocket);
this.terminal.loadAddon(this.attachAddon);
this.terminal.unicode.activeVersion = "11";
await new Promise((resolve, reject) => {
const websocket = new WebSocket(wsUrl);
const CONNECT_TIMEOUT = 5000;
let settled = false;
let hasOpened = false;

this.websocket = websocket;

const rejectInitialConnect = (message, error) => {
if (settled || hasOpened) return;
settled = true;
this.isConnected = false;
try {
websocket.close();
} catch {}
reject(error || new Error(message));
};

// Focus terminal and ensure it's ready
this.terminal.focus();
this.fit();
};
const connectionTimeout = setTimeout(() => {
rejectInitialConnect(
`Timed out while connecting to terminal session ${pid}`,
);
}, CONNECT_TIMEOUT);

websocket.onopen = () => {
clearTimeout(connectionTimeout);
hasOpened = true;
this.isConnected = true;
this.onConnect?.();

// Load attach addon after connection
this.attachAddon = new AttachAddon(websocket);
this.terminal.loadAddon(this.attachAddon);
this.terminal.unicode.activeVersion = "11";

// Focus terminal and ensure it's ready
this.terminal.focus();
this.fit();

if (!settled) {
settled = true;
resolve();
}
};

this.websocket.onmessage = (event) => {
// Handle text messages (exit events)
if (typeof event.data === "string") {
try {
const message = JSON.parse(event.data);
if (message.type === "exit") {
this.onProcessExit?.(message.data);
return;
websocket.onmessage = (event) => {
// Handle text messages (exit events)
if (typeof event.data === "string") {
try {
const message = JSON.parse(event.data);
if (message.type === "exit") {
this.onProcessExit?.(message.data);
return;
}
} catch (error) {
// Not a JSON message, let attachAddon handle it
}
} catch (error) {
// Not a JSON message, let attachAddon handle it
}
}
// For binary data or non-exit text messages, let attachAddon handle them
};
// For binary data or non-exit text messages, let attachAddon handle them
};

this.websocket.onclose = (event) => {
this.isConnected = false;
this.onDisconnect?.();
};
websocket.onclose = (event) => {
clearTimeout(connectionTimeout);
this.isConnected = false;

if (!hasOpened) {
const code = event?.code ? ` (code ${event.code})` : "";
const reason = event?.reason ? `: ${event.reason}` : "";
rejectInitialConnect(
`Terminal session ${pid} is unavailable${code}${reason}`,
);
return;
}

this.websocket.onerror = (error) => {
console.error("WebSocket error:", error);
this.onError?.(error);
};
this.onDisconnect?.();
};

websocket.onerror = (error) => {
if (!hasOpened) {
clearTimeout(connectionTimeout);
rejectInitialConnect(
`Failed to connect to terminal session ${pid}`,
new Error(`Failed to connect to terminal session ${pid}`),
);
return;
}
Comment thread
bajrangCoder marked this conversation as resolved.

console.error("WebSocket error:", error);
this.onError?.(error);
};
});
}

/**
Expand Down
151 changes: 118 additions & 33 deletions src/components/terminal/terminalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,31 +50,113 @@ class TerminalManager {
return nextNumber;
}

async getPersistedSessions() {
normalizePersistedSessions(stored) {
if (!Array.isArray(stored)) {
return {
sessions: [],
changed: stored != null,
};
}

const sessions = [];
const uniqueSessions = [];
const seenPids = new Set();
let changed = false;

for (const entry of stored) {
if (!entry) {
changed = true;
continue;
}

if (typeof entry === "string") {
sessions.push({
pid: entry,
name: `Terminal ${entry}`,
});
changed = true;
continue;
}

if (typeof entry !== "object" || !entry.pid) {
changed = true;
continue;
}

const pid = String(entry.pid);
const name =
typeof entry.name === "string" && entry.name.trim()
? entry.name.trim()
: `Terminal ${pid}`;

if (entry.pid !== pid || entry.name !== name) {
changed = true;
}

sessions.push({ pid, name });
}

for (const session of sessions) {
const pid = String(session.pid);
if (seenPids.has(pid)) {
changed = true;
continue;
}
seenPids.add(pid);
uniqueSessions.push({
pid,
name:
typeof session.name === "string" && session.name.trim()
? session.name.trim()
: `Terminal ${pid}`,
});
}

if (uniqueSessions.length !== stored.length) {
changed = true;
}

return {
sessions: uniqueSessions,
changed,
};
}

readPersistedSessions() {
try {
const stored = helpers.parseJSON(
localStorage.getItem(TERMINAL_SESSION_STORAGE_KEY),
return this.normalizePersistedSessions(
helpers.parseJSON(localStorage.getItem(TERMINAL_SESSION_STORAGE_KEY)),
);
if (!Array.isArray(stored)) return [];
} catch (error) {
console.error("Failed to read persisted terminal sessions:", error);
return {
sessions: [],
changed: false,
};
}
}

async getPersistedSessions() {
try {
const { sessions, changed } = this.readPersistedSessions();
if (!sessions.length) {
if (changed) {
this.savePersistedSessions([]);
}
return [];
}

if (!(await Terminal.isAxsRunning())) {
// Once the backend is gone, previously persisted PIDs are invalid.
this.savePersistedSessions([]);
return [];
}
return stored
.map((entry) => {
if (!entry) return null;
if (typeof entry === "string") {
return { pid: entry, name: `Terminal ${entry}` };
}
if (typeof entry === "object" && entry.pid) {
const pid = String(entry.pid);
return {
pid,
name: entry.name || `Terminal ${pid}`,
};
}
return null;
})
.filter(Boolean);

if (changed) {
this.savePersistedSessions(sessions);
}

return sessions;
} catch (error) {
console.error("Failed to read persisted terminal sessions:", error);
return [];
Expand All @@ -96,7 +178,7 @@ class TerminalManager {
if (!pid) return;

const pidStr = String(pid);
const sessions = await this.getPersistedSessions();
const { sessions } = this.readPersistedSessions();
const existingIndex = sessions.findIndex(
(session) => session.pid === pidStr,
);
Expand All @@ -121,7 +203,7 @@ class TerminalManager {
if (!pid) return;

const pidStr = String(pid);
const sessions = await this.getPersistedSessions();
const { sessions } = this.readPersistedSessions();
const nextSessions = sessions.filter((session) => session.pid !== pidStr);

if (nextSessions.length !== sessions.length) {
Expand Down Expand Up @@ -156,17 +238,17 @@ class TerminalManager {
error,
);
failedSessions.push(session.name || session.pid);
this.removePersistedSession(session.pid);
await this.removePersistedSession(session.pid);
}
}

// Show alert for failed sessions (don't await to not block UI)
// Stale session entries are expected after force-closes; keep startup quiet.
if (failedSessions.length > 0) {
const message =
failedSessions.length === 1
? `Failed to restore terminal: ${failedSessions[0]}`
: `Failed to restore ${failedSessions.length} terminals: ${failedSessions.join(", ")}`;
alert(strings["error"], message);
? `Skipped unavailable terminal: ${failedSessions[0]}`
: `Skipped ${failedSessions.length} unavailable terminals`;
toast(message);
}

if (activeFileId && manager?.getFile) {
Expand All @@ -184,9 +266,10 @@ class TerminalManager {
*/
async createTerminal(options = {}) {
try {
const { render, serverMode, ...terminalOptions } = options;
const { render, serverMode, reconnecting, ...terminalOptions } = options;
const shouldRender = render !== false;
const isServerMode = serverMode !== false;
const isReconnecting = reconnecting === true;

const terminalId = `terminal_${++this.terminalCounter}`;
const providedName =
Expand Down Expand Up @@ -305,11 +388,13 @@ class TerminalManager {
}

// Show alert for terminal creation failure
const errorMessage = error?.message || "Unknown error";
alert(
strings["error"],
`Failed to create terminal: ${errorMessage}`,
);
if (!isReconnecting) {
const errorMessage = error?.message || "Unknown error";
alert(
strings["error"],
`Failed to create terminal: ${errorMessage}`,
);
}

reject(error);
}
Expand Down
Loading