paris-immersion 0.1.3 → 0.1.5

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.
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { serveCommand } from './commands/serve.js';
12
12
  const program = new Command()
13
13
  .name('paris')
14
14
  .description('Paris Immersion — CLI do aluno (login, workspace, tarefas)')
15
- .version('0.1.3');
15
+ .version('0.1.5');
16
16
  program
17
17
  .command('login')
18
18
  .description('Autenticar com sua conta Paris (device-code OR --token direto)')
@@ -68,12 +68,44 @@ export async function startRelayAgent(opts) {
68
68
  let closed = false;
69
69
  let reconnect;
70
70
  let killTimer;
71
+ // Tamanho atual do pty (spawnCoachPty começa 80x24). Usado pra detectar resize
72
+ // "no-op" (reconexão com mesma dimensão) e forçar o Claude a repintar.
73
+ let ptyCols = 80;
74
+ let ptyRows = 24;
75
+ // Coalescing da saída do pty: o Claude emite a tela em RAJADAS de muitos
76
+ // chunks pequenos. Cifrar/enviar cada um (1 AES-GCM + 1 frame WS por chunk)
77
+ // é caro e deixa a escrita lenta. Junta numa janela curta → 1 frame por
78
+ // rajada. ~12ms é imperceptível e corta o overhead drasticamente.
79
+ const COALESCE_MS = 12;
80
+ const MAX_OUT_BUF = 64 * 1024;
81
+ let outBuf = '';
82
+ let flushTimer;
83
+ const flushOut = () => {
84
+ flushTimer = undefined;
85
+ if (!outBuf)
86
+ return;
87
+ const data = outBuf;
88
+ outBuf = '';
89
+ if (ws && ws.readyState === WebSocket.OPEN) {
90
+ try {
91
+ ws.send(seal(key, TYPE_DATA, Buffer.from(data, 'utf8')), { binary: true });
92
+ }
93
+ catch {
94
+ /* ws caiu no meio */
95
+ }
96
+ }
97
+ };
71
98
  // Mantém o Claude vivo por uma janela após o viewer sair, pra que uma
72
99
  // reconexão (ex.: Safari derruba o ws periodicamente) REATACHE na MESMA sessão
73
100
  // em vez de reiniciar o curso. O resize que o viewer manda ao reconectar faz a
74
101
  // TUI do Claude repintar a tela atual.
75
- const PTY_GRACE_MS = 60_000;
102
+ const PTY_GRACE_MS = 600_000; // 10min — aluno que sai no meio e volta reatacha na MESMA sessão
76
103
  const killPty = () => {
104
+ if (flushTimer) {
105
+ clearTimeout(flushTimer);
106
+ flushTimer = undefined;
107
+ }
108
+ outBuf = '';
77
109
  if (pty) {
78
110
  try {
79
111
  pty.kill();
@@ -95,17 +127,43 @@ export async function startRelayAgent(opts) {
95
127
  return;
96
128
  }
97
129
  pty.onData((d) => {
98
- if (ws && ws.readyState === WebSocket.OPEN) {
99
- try {
100
- ws.send(seal(key, TYPE_DATA, Buffer.from(d, 'utf8')), { binary: true });
101
- }
102
- catch {
103
- /* ws caiu no meio */
130
+ outBuf += d;
131
+ // Flush imediato se acumulou muito (evita um frame gigante); senão coalesce.
132
+ if (outBuf.length >= MAX_OUT_BUF) {
133
+ if (flushTimer) {
134
+ clearTimeout(flushTimer);
135
+ flushTimer = undefined;
104
136
  }
137
+ flushOut();
138
+ }
139
+ else if (!flushTimer) {
140
+ flushTimer = setTimeout(flushOut, COALESCE_MS);
105
141
  }
106
142
  });
107
143
  pty.onExit?.(() => killPty());
108
144
  };
145
+ // Força o Claude a repintar a tela INTEIRA (não só os diffs). Necessário quando
146
+ // um viewer reconecta com um xterm em branco e manda o MESMO tamanho — sem
147
+ // mudança de tamanho não há SIGWINCH, então o Claude não redesenha e a tela
148
+ // fica vazia/quebrada. Um "nudge" (uma linha a menos e volta) gera o SIGWINCH.
149
+ const forceRepaint = () => {
150
+ if (!pty || ptyCols < 1 || ptyRows < 2)
151
+ return;
152
+ try {
153
+ pty.resize(ptyCols, ptyRows - 1);
154
+ }
155
+ catch {
156
+ /* noop */
157
+ }
158
+ setTimeout(() => {
159
+ try {
160
+ pty?.resize(ptyCols, ptyRows);
161
+ }
162
+ catch {
163
+ /* noop */
164
+ }
165
+ }, 80);
166
+ };
109
167
  const connect = () => {
110
168
  if (closed)
111
169
  return;
@@ -148,13 +206,20 @@ export async function startRelayAgent(opts) {
148
206
  }
149
207
  else if (msg.type === TYPE_RESIZE) {
150
208
  const [cols, rows] = msg.body.toString('utf8').split(',').map(Number);
151
- if (Number.isFinite(cols) && Number.isFinite(rows)) {
209
+ if (Number.isFinite(cols) && Number.isFinite(rows) && cols > 0 && rows > 0) {
210
+ const same = cols === ptyCols && rows === ptyRows;
211
+ ptyCols = cols;
212
+ ptyRows = rows;
152
213
  try {
153
214
  pty.resize(cols, rows);
154
215
  }
155
216
  catch {
156
217
  /* race de resize */
157
218
  }
219
+ // Mesma dimensão (reconexão) não dispara SIGWINCH → o Claude não
220
+ // repinta e a tela fica em branco/quebrada. Força o repaint.
221
+ if (same)
222
+ forceRepaint();
158
223
  }
159
224
  }
160
225
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Paris Immersion CLI — login, install workspace, status, task control. Para alunos das imersões da Paris Group.",
5
5
  "homepage": "https://parisgroup.ai",
6
6
  "repository": {