@getparallel/parx 0.5.0 → 0.7.0

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/textos.js CHANGED
@@ -206,19 +206,62 @@ export const TEXTOS = {
206
206
  'salir.entorno': 'You are signed in with PARALLEL_API_KEY: remove it from your environment to sign out.',
207
207
  'salir.todas': 'Signed out on {n} terminals. API keys stay active.',
208
208
  'nombre.nuevo': 'parallel-code is now called parx. Type “parx”.',
209
+ 'estimacion.linea': '~{n} Actions (estimate)',
210
+ 'estimacion.con': '~{n} Actions (estimate) · with {modelo}: ~{otro}',
209
211
  'update.disponible': 'parx {version} available · run `parx update`',
210
212
  'update.al_dia': 'parx {version} is the latest version.',
211
213
  'update.sin_red': 'Could not reach npm to check for a new version. Try again later.',
212
- 'update.lanzando': 'Updating with: {orden}',
213
- 'update.hecho': 'Done. parx is now up to date.',
214
- 'update.fallo': 'The update did not finish. Run it yourself: {orden}',
215
214
  'update.npx': 'You are running parx through npx. Use `npx @getparallel/parx@latest` to always get the latest version.',
216
215
  'update.desarrollo': 'This is a development copy of parx (not installed from npm). Update it with git pull.',
216
+ 'update.actualizado': 'parx updated to {version}',
217
+ 'update.actualizado_con': 'parx updated to {version} · {novedad}',
218
+ 'update.preparada': 'parx {version} is ready · it runs the next time you open parx',
219
+ 'update.ya_preparada': 'parx {version} is already downloaded · it runs the next time you open parx',
220
+ 'update.no_arranco': 'parx {version} did not start · back on {actual}',
221
+ 'update.firma': 'parx {version} is on npm but its signature could not be verified, so it was not installed. Update by hand: {orden}',
222
+ 'update.no_se_pudo': 'parx {version} could not be downloaded or checked. Update by hand: {orden}',
223
+ 'autoupdate.on': 'parx keeps itself up to date: new versions are downloaded in the background and start with the next session.',
224
+ 'autoupdate.off': 'parx will not update itself. You will see a line when there is a new version; `parx update` installs it. /autoupdate on to turn it back on.',
225
+ 'remoto.abriendo': 'Turning on remote control…',
226
+ 'remoto.on': 'Remote control is on. Open this on your phone or any browser: {url}',
227
+ 'remoto.escanea': 'Or scan it with your phone:',
228
+ 'remoto.detalle': 'Everything still runs on this computer. Credentials and Bypass are only answered here.',
229
+ 'remoto.siempre': 'Remote control will turn on every time parx opens on this computer. /remote off to stop.',
230
+ 'remoto.off': 'Remote control is off on this computer.',
231
+ 'remoto.no_disponible': 'Remote control is not available for your account yet.',
232
+ 'remoto.sin_red': 'Could not turn on remote control: {motivo}. Try /remote again.',
233
+ 'remoto.cortado_web': 'Remote control was turned off from the web.',
234
+ 'remoto.sesion': 'Remote control stopped: sign in again with parx login.',
235
+ 'remoto.demasiados': 'You already have 12 parx sessions with remote control open. Close one and try again.',
236
+ 'remoto.del_movil': 'From your phone',
237
+ 'remoto.desde_el_movil': 'from your phone',
238
+ 'remoto.parado': 'Stopped from your phone.',
239
+ 'remoto.solo_interactiva': 'Remote control works in an interactive session: run parx and then /remote.',
240
+ 'mision.sugerencia': 'Next: /mission send it to my client · or announce it, or charge for it. It asks before sending anything.',
241
+ 'vista.lista': 'Preview on your phone (updates after each task): {url}',
242
+ 'vista.sin_remoto': 'Turn on /remote first: the preview opens from your phone.',
243
+ 'vista.sin_web': 'No web to preview: there is no index.html here or in dist/, build/ or out/.',
244
+ 'vista.no_subida': 'The preview could not be uploaded: {motivo}.',
245
+ 'servicio.on': 'Your Mac is ready. From Sessions on your phone you can start a task in: {carpetas}. It starts again every time you log in.',
246
+ 'servicio.off': 'The service is off. Your phone can no longer start tasks on this Mac.',
247
+ 'servicio.anadida': 'Added {ruta}. Your phone can start tasks there.',
248
+ 'servicio.no_anadida': '{ruta} cannot be added: it must be an existing folder, and not your whole home folder.',
249
+ 'servicio.quitada': 'Removed {ruta}.',
250
+ 'servicio.no_quitada': '{ruta} was not in the list.',
251
+ 'servicio.estado': 'Service: {estado}. Folders: {lista}.',
252
+ 'servicio.en_marcha': 'running',
253
+ 'servicio.parado': 'off',
254
+ 'servicio.ninguna': 'none',
255
+ 'servicio.sin_carpetas': 'Add a folder first: go to a project folder and run parx service add.',
256
+ 'servicio.solo_mac': 'The service is only for macOS for now.',
257
+ 'servicio.registro': 'Log: {ruta}',
258
+ 'servicio.uso': 'parx service on | off | add [folder] | remove <folder> | status',
217
259
  'error.sin_saldo': 'You are out of Actions. Top up or upgrade at getparallel.net/settings/billing',
218
260
  'error.sin_acceso': 'Your account does not have parx yet: it is in testing.',
219
261
  'error.sesion': 'Your session expired. Run: parx login',
220
262
  'error.red': 'Could not reach Parallel: {detalle}',
221
263
  'error.servidor': 'Parallel could not answer right now ({detalle}). Try again.',
264
+ 'error.ia': 'Parallel’s AI is temporarily unavailable. Your Actions weren’t charged. Try again in a minute.',
222
265
  'error.demasiado': 'This conversation is too long. Type /new to start another one.',
223
266
  'repl.adios': 'See you.',
224
267
  'repl.registro': 'Log of this session: {ruta}',
@@ -300,6 +343,8 @@ export const COMANDOS = [
300
343
  ['model', 'Swift, Stride or Summit'],
301
344
  ['folder', 'Let parx work in another folder'],
302
345
  ['lanes', 'Run 2 or 3 tasks at once'],
346
+ ['remote', 'Control this parx from your phone'],
347
+ ['preview', 'See the web it is building on your phone'],
303
348
  ['mission', 'Keep it running on Parallel, phone alert when done'],
304
349
  ['missions', 'Pick a mission and control it from here'],
305
350
  ['inbox', 'Answer what your missions are waiting for'],
@@ -315,6 +360,7 @@ export const COMANDOS = [
315
360
  ['tips', 'Tips when it opens: on or off'],
316
361
  ['mouse', 'Clicks and wheel in parx: on or off'],
317
362
  ['animation', 'Answers written out as they arrive: on or off'],
363
+ ['autoupdate', 'New versions install themselves: on or off'],
318
364
  ['log', 'Where the log of what it does is kept'],
319
365
  ['exit', 'Leave parx'],
320
366
  ];
@@ -325,6 +371,8 @@ export const ALIAS_COMANDO = {
325
371
  strength: 'model',
326
372
  folder: 'folder', 'add-folder': 'folder', carpeta: 'folder', dir: 'folder',
327
373
  lanes: 'lanes', carriles: 'lanes',
374
+ remote: 'remote', 'remote-control': 'remote', rc: 'remote', remoto: 'remote', phone: 'remote',
375
+ preview: 'preview', vista: 'preview', previa: 'preview',
328
376
  mission: 'mission', mision: 'mission', misión: 'mission',
329
377
  missions: 'missions', misiones: 'missions',
330
378
  inbox: 'inbox', bandeja: 'inbox', approvals: 'inbox', waiting: 'inbox',
@@ -333,6 +381,7 @@ export const ALIAS_COMANDO = {
333
381
  settings: 'settings', ajustes: 'settings', preferences: 'settings',
334
382
  mouse: 'mouse', raton: 'mouse', ratón: 'mouse',
335
383
  animation: 'animation', animations: 'animation', animacion: 'animation', animación: 'animation',
384
+ autoupdate: 'autoupdate', 'auto-update': 'autoupdate', autoupdates: 'autoupdate', updates: 'autoupdate', autoactualizar: 'autoupdate',
336
385
  report: 'report', informe: 'report',
337
386
  teach: 'teach', class: 'teach', clase: 'teach',
338
387
  undo: 'undo', deshacer: 'undo',
@@ -343,7 +392,7 @@ export const ALIAS_COMANDO = {
343
392
  log: 'log', registro: 'log',
344
393
  exit: 'exit', quit: 'exit', salir: 'exit',
345
394
  };
346
- export const ALIAS_DESDE = { strength: 2 };
395
+ export const ALIAS_DESDE = { strength: 2, rc: 2, phone: 3 };
347
396
  export const ALIAS_SI_NO = {
348
397
  on: 'si', yes: 'si', y: 'si', '1': 'si', si: 'si', 'sí': 'si',
349
398
  off: 'no', no: 'no', n: 'no', '0': 'no',
@@ -354,7 +403,7 @@ export const USO = [
354
403
  ' parx "task" do the task and exit',
355
404
  ' parx --lanes "task" "task" ... 2 or 3 tasks at once, each in its own lane',
356
405
  ' parx login | logout | account',
357
- ' parx update update parx to the latest version',
406
+ ' parx update get the latest version now (parx also updates itself; /autoupdate off stops that)',
358
407
  '',
359
408
  'Options:',
360
409
  ' --mode ask|auto|read Ask (default), Auto inside the project, Read only',
@@ -366,6 +415,7 @@ export const USO = [
366
415
  ' --ascii plain characters only (also NO_COLOR for no color)',
367
416
  ' --no-mouse leave the mouse to the terminal (no clicks or wheel in parx)',
368
417
  ' --no-animation show answers whole, without writing them out',
418
+ ' --remote control this session from your phone (same as /remote)',
369
419
  ' --version, --help',
370
420
  '',
371
421
  'Sign in:',
@@ -377,7 +427,18 @@ export const USO = [
377
427
  ' parx logout [--all] sign out here, or on every terminal',
378
428
  ' PARALLEL_API_KEY=par_key_… sign in with that key without saving anything (CI)',
379
429
  '',
430
+ 'Remote control:',
431
+ ' /remote link and QR to control this parx from your phone or a browser',
432
+ ' /remote always turn it on every time parx opens on this computer',
433
+ ' /remote off turn it off here',
434
+ ' parx service add let your phone start tasks in this folder (macOS)',
435
+ ' parx service on keep your Mac ready at login, even with parx closed',
436
+ ' parx service off | status stop it, or see how it is',
437
+ '',
380
438
  'Updates:',
381
- ' parx checks npm for a new version at most once a day, without slowing anything down.',
382
- ' PARX_NO_UPDATE_CHECK=1 turn that check off',
439
+ ' parx checks npm every few hours, without slowing anything down, and installs new versions',
440
+ ' itself: they are verified against npm’s signature and start with your next session.',
441
+ ' /autoupdate off only tell me when there is one (then: parx update)',
442
+ ' PARX_NO_AUTO_UPDATE=1 the same, from the environment',
443
+ ' PARX_NO_UPDATE_CHECK=1 never even check',
383
444
  ].join('\n');
package/dist/tui.js CHANGED
@@ -289,9 +289,9 @@ function teclaDeRaton(b, x, y, soltar, botonX10) {
289
289
  }
290
290
  export const RATON_SI = '\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h';
291
291
  export const RATON_NO = '\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l';
292
- const CON_VALOR = new Set(['mode', 'model', 'tips', 'notify', 'teach', 'mouse', 'animation']);
292
+ const CON_VALOR = new Set(['mode', 'model', 'tips', 'notify', 'teach', 'mouse', 'animation', 'autoupdate', 'remote']);
293
293
  const CON_TEXTO = new Set(['folder']);
294
- const SE_MANDAN_SOLOS = new Set(['teach', 'notify', 'tips', 'folder', 'mouse', 'animation']);
294
+ const SE_MANDAN_SOLOS = new Set(['teach', 'notify', 'tips', 'folder', 'mouse', 'animation', 'autoupdate', 'remote']);
295
295
  const MODOS_DEL_MENU = ['ask', 'auto', 'read', 'bypass'];
296
296
  export function opcionesDelMenu(valor) {
297
297
  if (!valor.startsWith('/') || valor.includes('\n'))
@@ -322,10 +322,16 @@ export function opcionesDelMenu(valor) {
322
322
  ? MODOS_DEL_MENU.map((k) => [k, tt(`modo.${k}.desc`)])
323
323
  : cmd === 'model'
324
324
  ? ORDEN_DE_MODELOS.map((k) => [k, `${MODELOS[k].nombre} ${MODELOS[k].version}: ${MODELOS[k].descripcion}`])
325
- : [
326
- ['on', cmd === 'teach' ? 'Explain each step as it codes' : cmd === 'notify' ? 'Notify me when it finishes or needs me' : cmd === 'mouse' ? 'Click options, scroll with the wheel' : cmd === 'animation' ? 'Write answers out as they arrive' : 'Show them when parx opens'],
327
- ['off', cmd === 'teach' ? 'Back to normal' : cmd === 'notify' ? 'No desktop notifications' : cmd === 'mouse' ? 'The terminal selects and scrolls as usual' : cmd === 'animation' ? 'Show answers whole, at once' : 'Don’t show them again'],
328
- ];
325
+ : cmd === 'remote'
326
+ ? [
327
+ ['on', 'Link and QR for your phone, for this session'],
328
+ ['always', 'Every time parx opens on this computer'],
329
+ ['off', 'Stop remote control here'],
330
+ ]
331
+ : [
332
+ ['on', cmd === 'teach' ? 'Explain each step as it codes' : cmd === 'notify' ? 'Notify me when it finishes or needs me' : cmd === 'mouse' ? 'Click options, scroll with the wheel' : cmd === 'animation' ? 'Write answers out as they arrive' : cmd === 'autoupdate' ? 'New versions install themselves, for the next session' : 'Show them when parx opens'],
333
+ ['off', cmd === 'teach' ? 'Back to normal' : cmd === 'notify' ? 'No desktop notifications' : cmd === 'mouse' ? 'The terminal selects and scrolls as usual' : cmd === 'animation' ? 'Show answers whole, at once' : cmd === 'autoupdate' ? 'Only tell me; I run parx update myself' : 'Don’t show them again'],
334
+ ];
329
335
  return valores
330
336
  .filter(([v]) => v.startsWith(q))
331
337
  .map(([v, d]) => ({ nombre: v, descripcion: d, completar: `${base}${v}`, enviar: true, ...(cmd === 'mode' ? { modo: v, ...(v === 'bypass' ? { peligro: true } : {}) } : {}) }));
@@ -547,7 +553,7 @@ export function componerCarril(d, ancho, alto) {
547
553
  cursor = null;
548
554
  }
549
555
  if (conPar)
550
- filaEscribir += ` ${cabezaDePar()} `;
556
+ filaEscribir += ` ${cabezaDePar(d.gesto)} `;
551
557
  const tinta = (t) => (bash ? c.bash(t) : c.violeta(t));
552
558
  const linea = conPar ? `${tinta(carril(util - ANCHO_DE_PAR - 2))}${caraDePar(d.gesto)}${tinta(carril(2))}` : tinta(carril(util));
553
559
  let barra;
@@ -1485,6 +1491,19 @@ export class Superficie {
1485
1491
  this.ponerValor(texto);
1486
1492
  this.enviar();
1487
1493
  }
1494
+ mandarDesdeFuera(texto) {
1495
+ const t = /^[/!]/.test(texto.trimStart()) ? `\u2060${texto.trimStart()}` : texto;
1496
+ if (this.resolverLectura && !this.trabajo && !this.pregunta) {
1497
+ const r = this.resolverLectura;
1498
+ this.resolverLectura = null;
1499
+ this.irAlFondo();
1500
+ this.anadir(this.bloqueDeEnvio(t));
1501
+ r(t);
1502
+ return;
1503
+ }
1504
+ this.cola.push(t);
1505
+ this.mostrarAviso(this.cola.length > 1 ? tt('tui.en_cola_n', { n: this.cola.length }) : tt('tui.en_cola'));
1506
+ }
1488
1507
  get libre() {
1489
1508
  return Boolean(this.resolverLectura) && !this.trabajo && !this.pregunta;
1490
1509
  }
@@ -1520,11 +1539,18 @@ export class Superficie {
1520
1539
  actualizarCarriles() {
1521
1540
  this.pintar();
1522
1541
  }
1523
- preguntar(texto, tipo = 'normal') {
1542
+ preguntar(texto, tipo = 'normal', externo) {
1524
1543
  const o = opcionesDePregunta(tipo);
1525
1544
  const peligro = tipo === 'bypass';
1526
1545
  return new Promise((resolver) => {
1527
- this.mostrarPregunta({
1546
+ let hecha = false;
1547
+ const contestar = (r) => {
1548
+ if (hecha)
1549
+ return;
1550
+ hecha = true;
1551
+ resolver(r);
1552
+ };
1553
+ const quitar = this.mostrarPregunta({
1528
1554
  texto,
1529
1555
  opciones: o.opciones,
1530
1556
  cortas: o.cortas,
@@ -1532,7 +1558,13 @@ export class Superficie {
1532
1558
  ...(peligro ? { peligro: true } : {}),
1533
1559
  indiceNo: o.valores.indexOf('no'),
1534
1560
  siConY: o.valores[o.elegida] !== 'no' || peligro,
1535
- }, (i) => resolver(i === null ? 'no' : (o.valores[i] ?? 'no')));
1561
+ }, (i) => contestar(i === null ? 'no' : (o.valores[i] ?? 'no')));
1562
+ externo?.then((r) => {
1563
+ if (hecha || !o.valores.includes(r))
1564
+ return;
1565
+ contestar(r);
1566
+ quitar();
1567
+ });
1536
1568
  });
1537
1569
  }
1538
1570
  elegir(texto, opciones, o = {}) {
@@ -1560,6 +1592,20 @@ export class Superficie {
1560
1592
  this.esperando.push(mostrar);
1561
1593
  else
1562
1594
  mostrar();
1595
+ return () => {
1596
+ const i = this.esperando.indexOf(mostrar);
1597
+ if (i >= 0) {
1598
+ this.esperando.splice(i, 1);
1599
+ return;
1600
+ }
1601
+ if (this.pregunta?.resolver !== resolver)
1602
+ return;
1603
+ this.pregunta = null;
1604
+ this.sobre = null;
1605
+ this.pulsado = null;
1606
+ this.pintar();
1607
+ this.esperando.shift()?.();
1608
+ };
1563
1609
  }
1564
1610
  responder(i) {
1565
1611
  const p = this.pregunta;
package/dist/ui.js CHANGED
@@ -47,6 +47,7 @@ export class Pantalla {
47
47
  esperaActiva = null;
48
48
  trabajoEnLinea = null;
49
49
  carril;
50
+ espejo = null;
50
51
  tty;
51
52
  constructor(rl, tty = Boolean(process.stdout.isTTY && process.stdin.isTTY), carril = null) {
52
53
  this.rl = rl;
@@ -110,6 +111,8 @@ export class Pantalla {
110
111
  this.pintarEnLinea();
111
112
  }
112
113
  escribir(linea = '', grupo = 'nota') {
114
+ if (linea.trim())
115
+ this.espejo?.evento('nota', { texto: sinColor(linea).trim() });
113
116
  if (!linea.trim()) {
114
117
  if (!this.terminaEnBlanco() && !this.carril)
115
118
  this.bloque(() => [''], this.ultimoGrupo ?? 'nota');
@@ -119,6 +122,7 @@ export class Pantalla {
119
122
  }
120
123
  lecturas = null;
121
124
  accion(verbo, objeto, nota = '') {
125
+ this.espejo?.evento('accion', { verbo: sinColor(verbo), objeto: sinColor(objeto), nota: sinColor(nota) });
122
126
  if (this.superficie && verbo === t('herr.leer')) {
123
127
  const l = this.lecturas;
124
128
  if (l && this.superficie.ultimoBloque() === l.bloque) {
@@ -146,6 +150,7 @@ export class Pantalla {
146
150
  .filter((l) => l.trim() !== '' && !RELLENO_DE_TESTS.test(l));
147
151
  if (!lineas.length)
148
152
  return;
153
+ this.espejo?.evento('salida', { lineas: lineas.slice(-6).map((l) => sinColor(l)) });
149
154
  const ultimas = lineas.slice(-maxLineas);
150
155
  const sobran = lineas.length - ultimas.length;
151
156
  this.bloque((ancho) => {
@@ -160,15 +165,19 @@ export class Pantalla {
160
165
  }, 'accion');
161
166
  }
162
167
  detalle(texto, error = false) {
168
+ this.espejo?.evento('detalle', { texto: sinColor(texto), error });
163
169
  this.bloque((ancho) => [`${' '.repeat(MARGEN + sangriaDeDetalle())}${recortar(error ? c.error(`${SIMBOLOS.falla} ${texto}`) : c.gris(texto), anchoUtil(ancho) - sangriaDeDetalle())}`], 'accion');
164
170
  }
165
171
  aviso(texto) {
172
+ this.espejo?.evento('aviso', { texto: sinColor(texto) });
166
173
  this.bloque(bloqueDeTexto(`${c.aviso('!')} ${texto}`, ' '), 'aviso');
167
174
  }
168
175
  error(texto) {
176
+ this.espejo?.evento('error', { texto: sinColor(texto) });
169
177
  this.bloque(bloqueDeTexto(`${c.error(SIMBOLOS.falla)} ${texto}`, ' '), 'error');
170
178
  }
171
179
  cierre(texto) {
180
+ this.espejo?.evento('cierre', { texto: sinColor(texto) });
172
181
  this.bloque(bloqueDeCierre(texto, c.ok(SIMBOLOS.hecho)), 'cierre');
173
182
  }
174
183
  trabajoPropio() {
@@ -205,6 +214,7 @@ export class Pantalla {
205
214
  process.stdout.write(`\r\x1b[2K${' '.repeat(MARGEN)}${recortar(filaDeTrabajo(this.trabajoEnLinea, Date.now()), ancho - MARGEN - 2)}`);
206
215
  }
207
216
  progreso(acciones) {
217
+ this.espejo?.progreso(acciones);
208
218
  if (this.carril) {
209
219
  this.carril.trabajo.acciones = acciones;
210
220
  this.superficie?.actualizarCarriles();
@@ -236,6 +246,10 @@ export class Pantalla {
236
246
  }
237
247
  diff(lineas, max = 40) {
238
248
  const visibles = trozos(lineas);
249
+ this.espejo?.evento('diff', {
250
+ lineas: visibles.slice(0, 40).map((l) => (l === null ? { tipo: '⋮', texto: '' } : { tipo: l.tipo, texto: l.texto, num: l.despues ?? l.antes ?? null })),
251
+ mas: Math.max(0, visibles.length - 40),
252
+ });
239
253
  this.bloque((ancho) => {
240
254
  const s = sangriaDeDetalle();
241
255
  const util = anchoUtil(ancho) - s;
@@ -270,7 +284,21 @@ export class Pantalla {
270
284
  this.aviso(o.aviso);
271
285
  const texto = this.carril ? `${t('carriles.carril', { n: this.carril.numero })}: ${pregunta}` : pregunta;
272
286
  if (this.superficie) {
273
- const r = await this.superficie.preguntar(texto, tipo);
287
+ const op = opcionesDePregunta(tipo);
288
+ const remota = this.espejo?.preguntaRemota(sinColor(texto), tipo, op.opciones.map((etiqueta, i) => ({ etiqueta, valor: op.valores[i] }))) ?? null;
289
+ let desdeFuera = false;
290
+ const externo = remota?.deFuera?.then((r) => {
291
+ desdeFuera = true;
292
+ return r;
293
+ });
294
+ const r = await this.superficie.preguntar(texto, tipo, externo);
295
+ remota?.contestada(r, desdeFuera ? 'movil' : 'ordenador');
296
+ if (desdeFuera) {
297
+ const o2 = opcionesDePregunta(tipo);
298
+ const etiqueta = o2.opciones[o2.valores.indexOf(r)] ?? t('tui.no');
299
+ this.bloque(bloqueDeTexto(`${c.gris('?')} ${texto} ${c.gris('→')} ${r === 'no' ? c.gris(etiqueta) : c.marca(etiqueta)} ${c.gris(`· ${t('remoto.desde_el_movil')}`)}`, ' '), 'aviso');
300
+ return r;
301
+ }
274
302
  if (r !== 'si' && tipo !== 'bypass') {
275
303
  const o2 = opcionesDePregunta(tipo);
276
304
  const etiqueta = o2.opciones[o2.valores.indexOf(r)] ?? t('tui.no');
@@ -280,6 +308,16 @@ export class Pantalla {
280
308
  }
281
309
  this.pausarLinea();
282
310
  if (!this.tty || !this.rl) {
311
+ if (this.espejo) {
312
+ const op = opcionesDePregunta(tipo);
313
+ const remota = this.espejo.preguntaRemota(sinColor(texto), tipo, op.opciones.map((etiqueta, i) => ({ etiqueta, valor: op.valores[i] })));
314
+ if (remota.deFuera) {
315
+ const r = await remota.deFuera;
316
+ remota.contestada(r, 'movil');
317
+ return r;
318
+ }
319
+ remota.contestada('no', 'ordenador');
320
+ }
283
321
  this.aviso(`${texto} → ${t('aviso.rechazado')}`);
284
322
  return 'no';
285
323
  }
@@ -293,6 +331,10 @@ export class Pantalla {
293
331
  }
294
332
  }
295
333
  texto(md) {
334
+ this.espejo?.evento('texto', { md });
335
+ this.pintarTexto(md);
336
+ }
337
+ pintarTexto(md) {
296
338
  this.terminarRevelado();
297
339
  this.bloque(bloqueDeMarkdown(md), 'texto');
298
340
  }
@@ -304,10 +346,11 @@ export class Pantalla {
304
346
  f?.();
305
347
  }
306
348
  textoAnimado(md, o = {}) {
349
+ this.espejo?.evento('texto', { md });
307
350
  this.terminarRevelado();
308
351
  const conSuperficie = this.superficie !== null;
309
352
  if (!this.animar || this.carril || !this.tty || (!conSuperficie && !process.stdout.isTTY)) {
310
- this.texto(md);
353
+ this.pintarTexto(md);
311
354
  return Promise.resolve();
312
355
  }
313
356
  const completo = bloqueDeMarkdown(md);