sapper-iq 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sapper-ui.mjs +88 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
package/sapper-ui.mjs CHANGED
@@ -3126,41 +3126,98 @@ function spawnSapper(cols, rows) {
3126
3126
  });
3127
3127
  }
3128
3128
 
3129
+ // ─── Persistent PTY (survives browser refresh) ───────────────────
3130
+ // The pty process lives at module scope so it outlives any WS connection.
3131
+ // All output is stored in a ring buffer; new clients replay it on connect.
3132
+
3133
+ const PTY_SCROLLBACK_MAX = 512 * 1024; // 512 KB replay buffer
3134
+ let sharedPty = null;
3135
+ let ptyScrollback = ''; // raw bytes (utf-8) for replay
3136
+ let ptyCols = 220, ptyRows = 50; // last known size
3137
+
3138
+ function appendPtyScrollback(chunk) {
3139
+ ptyScrollback += chunk;
3140
+ if (ptyScrollback.length > PTY_SCROLLBACK_MAX) {
3141
+ // Drop oldest half to stay near cap without constant slicing
3142
+ ptyScrollback = ptyScrollback.slice(ptyScrollback.length - Math.floor(PTY_SCROLLBACK_MAX * 0.6));
3143
+ }
3144
+ }
3145
+
3146
+ function ensurePty(cols, rows) {
3147
+ if (sharedPty) return;
3148
+ ptyCols = cols || 220; ptyRows = rows || 50;
3149
+ try {
3150
+ sharedPty = spawnSapper(ptyCols, ptyRows);
3151
+ } catch (e) {
3152
+ console.error('[ui] spawn failed:', e.message);
3153
+ return;
3154
+ }
3155
+ dbg('pty pid=' + sharedPty.pid + ' ' + ptyCols + 'x' + ptyRows);
3156
+ sharedPty.onData((d) => {
3157
+ appendPtyScrollback(d);
3158
+ // Broadcast to every connected pty client
3159
+ for (const ws of ptyClients) {
3160
+ if (ws.readyState === ws.OPEN) {
3161
+ try { ws.send(Buffer.from(d, 'utf8')); } catch {}
3162
+ }
3163
+ }
3164
+ });
3165
+ sharedPty.onExit(({ exitCode, signal }) => {
3166
+ dbg('pty exit code=' + exitCode);
3167
+ sharedPty = null;
3168
+ // Notify all clients so they can show "exited" badge
3169
+ const msg = JSON.stringify({ type: 'exit', code: exitCode, signal });
3170
+ for (const ws of ptyClients) {
3171
+ if (ws.readyState === ws.OPEN) { try { ws.send(msg); } catch {} }
3172
+ }
3173
+ });
3174
+ }
3175
+
3176
+ const ptyClients = new Set(); // all currently-connected pty websockets
3177
+
3129
3178
  wssPty.on('connection', (ws) => {
3130
3179
  dbg('pty client connected');
3131
- let pty = null; let initialized = false;
3132
-
3133
- function start(cols, rows) {
3134
- if (pty) { try { pty.kill(); } catch {} }
3135
- try { pty = spawnSapper(cols, rows); }
3136
- catch (e) {
3137
- console.error('[ui] spawn failed:', e.message);
3138
- try { ws.send(Buffer.from('\x1b[31mFailed to spawn sapper: ' + e.message + '\x1b[0m\r\n', 'utf8')); } catch {}
3139
- return;
3140
- }
3141
- dbg('pty pid=' + pty.pid + ' ' + cols + 'x' + rows);
3142
- pty.onData((d) => { if (ws.readyState === ws.OPEN) ws.send(Buffer.from(d, 'utf8')); });
3143
- pty.onExit(({ exitCode, signal }) => {
3144
- dbg('pty exit code=' + exitCode);
3145
- if (ws.readyState === ws.OPEN) { try { ws.send(JSON.stringify({ type: 'exit', code: exitCode, signal })); } catch {} }
3146
- });
3147
- try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
3148
- }
3180
+ ptyClients.add(ws);
3149
3181
 
3150
3182
  ws.on('message', (raw, isBinary) => {
3151
3183
  const str = raw.toString('utf8');
3152
3184
  if (!isBinary && str.startsWith('{')) {
3153
3185
  try {
3154
3186
  const m = JSON.parse(str);
3155
- if (m.type === 'init') { if (!initialized) { initialized = true; start(m.cols, m.rows); } return; }
3156
- if (m.type === 'resize' && pty) { try { pty.resize(m.cols || 100, m.rows || 30); } catch {} return; }
3157
- if (m.type === 'restart') { initialized = true; start(100, 30); return; }
3187
+ if (m.type === 'init') {
3188
+ // Spawn pty if not running yet
3189
+ ensurePty(m.cols, m.rows);
3190
+ // Replay scrollback so the refreshed browser sees prior output
3191
+ if (ptyScrollback.length > 0) {
3192
+ try { ws.send(Buffer.from(ptyScrollback, 'utf8')); } catch {}
3193
+ }
3194
+ // Always send current cwd
3195
+ try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
3196
+ return;
3197
+ }
3198
+ if (m.type === 'resize' && sharedPty) {
3199
+ ptyCols = m.cols || ptyCols; ptyRows = m.rows || ptyRows;
3200
+ try { sharedPty.resize(ptyCols, ptyRows); } catch {}
3201
+ return;
3202
+ }
3203
+ if (m.type === 'restart') {
3204
+ // Kill current pty and start fresh; clear scrollback
3205
+ if (sharedPty) { try { sharedPty.kill(); } catch {} sharedPty = null; }
3206
+ ptyScrollback = '';
3207
+ ensurePty(ptyCols, ptyRows);
3208
+ try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
3209
+ return;
3210
+ }
3158
3211
  } catch {}
3159
3212
  }
3160
- if (pty) pty.write(str);
3213
+ if (sharedPty) sharedPty.write(str);
3161
3214
  });
3162
3215
 
3163
- ws.on('close', () => { if (pty) { try { pty.kill(); } catch {} pty = null; } });
3216
+ ws.on('close', () => {
3217
+ ptyClients.delete(ws);
3218
+ // Do NOT kill the pty — keep it alive for the next reconnect.
3219
+ dbg('pty client disconnected (' + ptyClients.size + ' remaining)');
3220
+ });
3164
3221
  });
3165
3222
 
3166
3223
  // ── FS watcher: broadcast to all /events clients ─────────────────
@@ -3437,5 +3494,11 @@ server.on('listening', () => {
3437
3494
 
3438
3495
  tryListen(PORT);
3439
3496
 
3440
- process.on('SIGINT', () => { console.log('\nShutting down…'); try { watcher && watcher.close(); } catch {} process.exit(0); });
3441
- process.on('SIGTERM', () => process.exit(0));
3497
+ function shutdown() {
3498
+ console.log('\nShutting down…');
3499
+ try { watcher && watcher.close(); } catch {}
3500
+ if (sharedPty) { try { sharedPty.kill(); } catch {} }
3501
+ process.exit(0);
3502
+ }
3503
+ process.on('SIGINT', shutdown);
3504
+ process.on('SIGTERM', shutdown);