• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 22173138776

19 Feb 2026 07:50AM UTC coverage: 81.939% (+0.9%) from 81.026%
22173138776

push

github

apowers313
fix: status indicators when shell is deselected, new shell death bugs

2162 of 2510 branches covered (86.14%)

Branch coverage included in aggregate %.

50 of 175 new or added lines in 10 files covered. (28.57%)

214 existing lines in 10 files now uncovered.

10269 of 12661 relevant lines covered (81.11%)

27.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.0
/src/server/websocket/WebSocketServer.ts
1
/**
2
 * WebSocketServer - WebSocket server for terminal I/O
3
 */
4
import { WebSocketServer as WsServer, type WebSocket } from 'ws';
1✔
5
import type { Server as HttpServer } from 'http';
6
import { HeartbeatManager, type HeartbeatWebSocketServer } from './heartbeat.js';
1✔
7
import { TerminalHandler, type TerminalMessage } from './handlers/terminal.js';
1✔
8
import type { ShellSessionManager } from '@server/services/shell/ShellSessionManager.js';
9
import type { ShellActivityMessage } from '@shared/types/index.js';
10
import { logger } from '@server/utils/logger.js';
1✔
11

12
/**
13
 * Debounce interval for shell activity broadcasts (ms).
14
 * Each shell gets at most one broadcast per interval.
15
 */
16
const ACTIVITY_BROADCAST_DEBOUNCE_MS = 1000;
1✔
17

18
/**
19
 * Extended WebSocket with isAlive tracking
20
 */
21
export interface ExtendedWebSocket extends WebSocket {
22
  isAlive: boolean;
23
}
24

25
/**
26
 * Options for creating a WebSocket server
27
 */
28
export interface WebSocketServerOptions {
29
  server: HttpServer;
30
  sessionManager: ShellSessionManager;
31
  path?: string;
32
  heartbeatInterval?: number;
33
}
34

35
/**
36
 * Create and configure a WebSocket server for terminal communication
37
 */
38
export function createWebSocketServer(options: WebSocketServerOptions): WsServer {
1✔
39
  const {
10✔
40
    server,
10✔
41
    sessionManager,
10✔
42
    path = '/ws/terminal',
10✔
43
    heartbeatInterval = 30000,
10✔
44
  } = options;
10✔
45

46
  // Create WebSocket server
47
  const wss = new WsServer({ server, path });
10✔
48

49
  // Create terminal handler
50
  const terminalHandler = new TerminalHandler(sessionManager);
10✔
51

52
  // Create and start heartbeat manager
53
  const heartbeatManager = new HeartbeatManager(
10✔
54
    wss as unknown as HeartbeatWebSocketServer,
10✔
55
    { interval: heartbeatInterval },
10✔
56
  );
10✔
57
  heartbeatManager.start();
10✔
58

59
  // Set up debounced activity broadcasts to all connected clients.
60
  // When any shell has PTY activity, broadcast a lightweight notification
61
  // so clients can update activity indicators for shells they aren't viewing.
62
  const activityDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
10✔
63
  sessionManager.on('session:activity', (shellId: string) => {
10✔
NEW
64
    if (activityDebounceTimers.has(shellId)) {
×
NEW
65
      return; // Already scheduled, skip
×
NEW
66
    }
×
NEW
67
    const timer = setTimeout(() => {
×
NEW
68
      activityDebounceTimers.delete(shellId);
×
NEW
69
      const message: ShellActivityMessage = { type: 'shell.activity', shellId };
×
NEW
70
      const payload = JSON.stringify(message);
×
NEW
71
      for (const client of wss.clients) {
×
NEW
72
        if (client.readyState === 1) { // WebSocket.OPEN
×
NEW
73
          client.send(payload);
×
NEW
74
        }
×
NEW
75
      }
×
NEW
76
    }, ACTIVITY_BROADCAST_DEBOUNCE_MS);
×
NEW
77
    activityDebounceTimers.set(shellId, timer);
×
78
  });
10✔
79

80
  // Handle new connections
81
  wss.on('connection', (ws: ExtendedWebSocket) => {
10✔
82
    logger.debug('WebSocket client connected');
6✔
83

84
    ws.isAlive = true;
6✔
85

86
    // Handle pong responses
87
    ws.on('pong', () => {
6✔
88
      ws.isAlive = true;
1✔
89
    });
6✔
90

91
    // Handle incoming messages
92
    ws.on('message', (data: Buffer) => {
6✔
93
      try {
2✔
94
        const message = JSON.parse(data.toString('utf8')) as TerminalMessage;
2✔
95
        terminalHandler.handleMessage(ws, message);
2✔
96
      } catch (err) {
2✔
97
        logger.error({ err }, 'Failed to parse WebSocket message');
1✔
98
        ws.send(JSON.stringify({
1✔
99
          type: 'error',
1✔
100
          code: 'INVALID_MESSAGE',
1✔
101
          message: 'Invalid message format',
1✔
102
        }));
1✔
103
      }
1✔
104
    });
6✔
105

106
    // Handle close
107
    ws.on('close', () => {
6✔
108
      logger.debug('WebSocket client disconnected');
1✔
109
      terminalHandler.handleDisconnect(ws);
1✔
110
    });
6✔
111

112
    // Handle errors
113
    ws.on('error', (err) => {
6✔
114
      logger.error({ err }, 'WebSocket error');
1✔
115
    });
6✔
116
  });
10✔
117

118
  // Handle server close
119
  wss.on('close', () => {
10✔
120
    heartbeatManager.stop();
1✔
121
  });
10✔
122

123
  logger.info({ path }, 'WebSocket server started');
10✔
124

125
  return wss;
10✔
126
}
10✔
127

128
/**
129
 * WebSocket server wrapper for easier integration
130
 */
131
export class TerminalWebSocketServer {
1✔
132
  private readonly _wss: WsServer;
4✔
133
  private readonly _heartbeatManager: HeartbeatManager;
4✔
134
  private readonly _terminalHandler: TerminalHandler;
4✔
135

136
  constructor(options: WebSocketServerOptions) {
4✔
137
    const {
4✔
138
      server,
4✔
139
      sessionManager,
4✔
140
      path = '/ws/terminal',
4✔
141
      heartbeatInterval = 30000,
4✔
142
    } = options;
4✔
143

144
    // Create WebSocket server
145
    this._wss = new WsServer({ server, path });
4✔
146

147
    // Create terminal handler
148
    this._terminalHandler = new TerminalHandler(sessionManager);
4✔
149

150
    // Create heartbeat manager
151
    this._heartbeatManager = new HeartbeatManager(
4✔
152
      this._wss as unknown as HeartbeatWebSocketServer,
4✔
153
      { interval: heartbeatInterval },
4✔
154
    );
4✔
155

156
    this._setupHandlers();
4✔
157
  }
4✔
158

159
  private _setupHandlers(): void {
4✔
160
    // Handle new connections
161
    this._wss.on('connection', (ws: ExtendedWebSocket) => {
4✔
162
      logger.debug('WebSocket client connected');
1✔
163

164
      ws.isAlive = true;
1✔
165

166
      // Handle pong responses
167
      ws.on('pong', () => {
1✔
168
        ws.isAlive = true;
×
169
      });
1✔
170

171
      // Handle incoming messages
172
      ws.on('message', (data: Buffer) => {
1✔
173
        try {
×
174
          const message = JSON.parse(data.toString('utf8')) as TerminalMessage;
×
175
          this._terminalHandler.handleMessage(ws, message);
×
176
        } catch (err) {
×
177
          logger.error({ err }, 'Failed to parse WebSocket message');
×
178
          ws.send(JSON.stringify({
×
179
            type: 'error',
×
180
            code: 'INVALID_MESSAGE',
×
181
            message: 'Invalid message format',
×
182
          }));
×
183
        }
×
184
      });
1✔
185

186
      // Handle close
187
      ws.on('close', () => {
1✔
188
        logger.debug('WebSocket client disconnected');
×
189
        this._terminalHandler.handleDisconnect(ws);
×
190
      });
1✔
191

192
      // Handle errors
193
      ws.on('error', (err) => {
1✔
194
        logger.error({ err }, 'WebSocket error');
×
195
      });
1✔
196
    });
4✔
197
  }
4✔
198

199
  /**
200
   * Start the heartbeat
201
   */
202
  start(): void {
4✔
203
    this._heartbeatManager.start();
1✔
204
    logger.info('WebSocket server heartbeat started');
1✔
205
  }
1✔
206

207
  /**
208
   * Stop the heartbeat and close all connections
209
   */
210
  stop(): void {
4✔
211
    this._heartbeatManager.stop();
1✔
212
    this._wss.close();
1✔
213
    logger.info('WebSocket server stopped');
1✔
214
  }
1✔
215

216
  /**
217
   * Get the underlying WebSocket server
218
   */
219
  get wss(): WsServer {
4✔
220
    return this._wss;
2✔
221
  }
2✔
222
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc