@getparallel/parx 0.4.4 → 0.6.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/tar.js ADDED
@@ -0,0 +1,99 @@
1
+ import { gunzipSync } from 'node:zlib';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, normalize, sep } from 'node:path';
4
+ const BLOQUE = 512;
5
+ function campo(b, desde, largo) {
6
+ const fin = b.indexOf(0, desde);
7
+ return b.subarray(desde, fin === -1 || fin > desde + largo ? desde + largo : fin).toString('utf8');
8
+ }
9
+ function numero(b, desde, largo) {
10
+ const s = campo(b, desde, largo).trim();
11
+ if (!s)
12
+ return 0;
13
+ if (b[desde] & 0x80) {
14
+ let n = 0;
15
+ for (let i = 1; i < largo; i++)
16
+ n = n * 256 + b[desde + i];
17
+ return n;
18
+ }
19
+ const n = parseInt(s, 8);
20
+ return Number.isFinite(n) ? n : 0;
21
+ }
22
+ export function leerTar(tar, limites = {}) {
23
+ const maxFicheros = limites.maxFicheros ?? 400;
24
+ const maxBytes = limites.maxBytes ?? 20 * 1024 * 1024;
25
+ const prefijo = limites.prefijo ?? 'package/';
26
+ const salida = [];
27
+ let bytes = 0;
28
+ let pos = 0;
29
+ let nombreLargo = null;
30
+ let rutaPax = null;
31
+ while (pos + BLOQUE <= tar.length) {
32
+ const cab = tar.subarray(pos, pos + BLOQUE);
33
+ if (cab.every((x) => x === 0))
34
+ break;
35
+ const tipo = String.fromCharCode(cab[156]);
36
+ const tamano = numero(cab, 124, 12);
37
+ const datos = tar.subarray(pos + BLOQUE, pos + BLOQUE + tamano);
38
+ pos += BLOQUE + Math.ceil(tamano / BLOQUE) * BLOQUE;
39
+ if (tipo === 'L') {
40
+ nombreLargo = datos.toString('utf8').replace(/\0+$/, '');
41
+ continue;
42
+ }
43
+ if (tipo === 'x') {
44
+ for (const linea of datos.toString('utf8').split('\n')) {
45
+ const m = /^\d+ path=(.*)$/.exec(linea);
46
+ if (m)
47
+ rutaPax = m[1];
48
+ }
49
+ continue;
50
+ }
51
+ if (tipo === 'g')
52
+ continue;
53
+ let nombre = rutaPax ?? nombreLargo ?? campo(cab, 0, 100);
54
+ if (!rutaPax && !nombreLargo && campo(cab, 257, 6) === 'ustar') {
55
+ const pre = campo(cab, 345, 155);
56
+ if (pre)
57
+ nombre = `${pre}/${nombre}`;
58
+ }
59
+ rutaPax = null;
60
+ nombreLargo = null;
61
+ if (tipo === '5' || nombre.endsWith('/'))
62
+ continue;
63
+ if (tipo !== '0' && tipo !== '\0' && tipo !== '7')
64
+ throw new Error(`unsupported entry type "${tipo}" for ${nombre}`);
65
+ if (!nombre.startsWith(prefijo))
66
+ throw new Error(`entry outside the package: ${nombre}`);
67
+ const ruta = nombre.slice(prefijo.length);
68
+ if (!rutaSegura(ruta))
69
+ throw new Error(`unsafe path: ${nombre}`);
70
+ bytes += datos.length;
71
+ if (salida.length >= maxFicheros || bytes > maxBytes)
72
+ throw new Error('package too large');
73
+ salida.push({ ruta, contenido: Buffer.from(datos) });
74
+ }
75
+ return salida;
76
+ }
77
+ export function rutaSegura(ruta) {
78
+ if (!ruta || ruta.includes('\\') || ruta.includes('\0') || ruta.startsWith('/'))
79
+ return false;
80
+ if (/^[A-Za-z]:/.test(ruta))
81
+ return false;
82
+ const partes = ruta.split('/');
83
+ return partes.every((p) => p && p !== '.' && p !== '..');
84
+ }
85
+ export function leerTgz(tgz, limites = {}) {
86
+ const maxBytes = limites.maxBytes ?? 20 * 1024 * 1024;
87
+ const tar = gunzipSync(tgz, { maxOutputLength: maxBytes + 2 * 1024 * 1024 });
88
+ return leerTar(tar, limites);
89
+ }
90
+ export function escribirFicheros(ficheros, destino) {
91
+ const base = normalize(destino + sep);
92
+ for (const f of ficheros) {
93
+ const ruta = normalize(join(destino, ...f.ruta.split('/')));
94
+ if (!ruta.startsWith(base))
95
+ throw new Error(`unsafe path: ${f.ruta}`);
96
+ mkdirSync(dirname(ruta), { recursive: true });
97
+ writeFileSync(ruta, f.contenido, { mode: f.ruta.endsWith('.js') ? 0o755 : 0o644 });
98
+ }
99
+ }
package/dist/textos.js CHANGED
@@ -21,6 +21,7 @@ export const TEXTOS = {
21
21
  'tui.una_vez': 'Yes, this once',
22
22
  'tui.atajos': 'Shift+Tab mode · ↑ ↓ history · ! runs a shell command · Esc stops a task · any key shows the whole answer · Ctrl+C twice exits · Ctrl+L redraws',
23
23
  'tui.atajos_raton': 'Click any option or button · wheel or PgUp/PgDn scrolls · End jumps to the bottom · Shift+drag selects text (Option in iTerm2, Fn in Terminal) · /mouse off gives the mouse back to the terminal',
24
+ 'tui.atajos_arroba': '@ names one of your Parallel apps · one that is not connected opens its Connect page',
24
25
  'tui.al_fondo': 'Jump to bottom',
25
26
  'tui.nuevas': '{n} new',
26
27
  'bash.ejemplo': 'A shell command, run in the project folder',
@@ -53,6 +54,46 @@ export const TEXTOS = {
53
54
  'bandeja.despacio': 'Too many answers at once. Try again in a minute.',
54
55
  'bandeja.no_disponible': 'Your missions are not reachable from parx yet.',
55
56
  'bandeja.abrir': 'Open in the browser: {url}',
57
+ 'arroba.conectada': 'Connected',
58
+ 'arroba.conectar': 'Connect',
59
+ 'arroba.abriendo': 'Connect {nombre} in your browser: {url}',
60
+ 'arroba.despues': 'Then type @ again: it shows up as connected.',
61
+ 'misiones.titulo': 'Pick a mission',
62
+ 'misiones.mirando': 'Looking at your missions',
63
+ 'misiones.nada': 'You have no missions yet. Start one with /mission <goal>.',
64
+ 'misiones.sin_coincidencias': 'No mission matches “{q}”.',
65
+ 'misiones.volver': 'Back',
66
+ 'misiones.progreso': '{estado} · {n}%',
67
+ 'misiones.esperan': '{n} waiting',
68
+ 'misiones.abierta': 'Opened in your browser: {url}',
69
+ 'misiones.pausada': '“{titulo}” paused',
70
+ 'misiones.reanudada': '“{titulo}” is running again',
71
+ 'misiones.no_se_puede': 'That mission cannot do that right now.',
72
+ 'misiones.no_existe': 'That mission no longer exists.',
73
+ 'misiones.compartida': 'Read-only link, copied to the clipboard: {url}',
74
+ 'misiones.compartida_sin_copiar': 'Read-only link: {url}',
75
+ 'misiones.sin_compartir': 'Sharing is not available right now.',
76
+ 'misiones.resultado': 'Result',
77
+ 'misiones.meta': 'Goal',
78
+ 'misiones.objetivo': '{etiqueta}: {valor} of {meta}',
79
+ 'misiones.sin_objetivo': 'Not measured yet',
80
+ 'misiones.el_plan': 'Plan',
81
+ 'misiones.plan': '{estado} · {n}% done',
82
+ 'misiones.ver_resultados': 'Results: {url}',
83
+ 'misiones.actividad': 'Activity',
84
+ 'misiones.sin_actividad': 'Nothing yet: the agents have not started.',
85
+ 'misiones.ver_actividad': 'All of it: {url}',
86
+ 'misiones.no_disponible': 'Your missions are not reachable from parx yet.',
87
+ 'misiones.despacio': 'Too many at once. Try again in a minute.',
88
+ 'plantillas.titulo': 'Start from a template',
89
+ 'plantillas.mirando': 'Looking at the templates',
90
+ 'plantillas.nada': 'No templates right now.',
91
+ 'plantillas.sin_coincidencias': 'No template matches “{q}”.',
92
+ 'plantillas.propia': 'Yours',
93
+ 'plantillas.lista': 'Written below as a mission: review it and press Enter.',
94
+ 'ir.abriendo': 'Opening {que} in your browser: {url}',
95
+ 'ir.conexiones': 'your connections',
96
+ 'ir.ajustes': 'your settings',
56
97
  'modo.ask': 'Ask',
57
98
  'modo.auto': 'Auto',
58
99
  'modo.read': 'Read only',
@@ -165,19 +206,43 @@ export const TEXTOS = {
165
206
  'salir.entorno': 'You are signed in with PARALLEL_API_KEY: remove it from your environment to sign out.',
166
207
  'salir.todas': 'Signed out on {n} terminals. API keys stay active.',
167
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}',
168
211
  'update.disponible': 'parx {version} available · run `parx update`',
169
212
  'update.al_dia': 'parx {version} is the latest version.',
170
213
  'update.sin_red': 'Could not reach npm to check for a new version. Try again later.',
171
- 'update.lanzando': 'Updating with: {orden}',
172
- 'update.hecho': 'Done. parx is now up to date.',
173
- 'update.fallo': 'The update did not finish. Run it yourself: {orden}',
174
214
  'update.npx': 'You are running parx through npx. Use `npx @getparallel/parx@latest` to always get the latest version.',
175
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.',
176
240
  'error.sin_saldo': 'You are out of Actions. Top up or upgrade at getparallel.net/settings/billing',
177
241
  'error.sin_acceso': 'Your account does not have parx yet: it is in testing.',
178
242
  'error.sesion': 'Your session expired. Run: parx login',
179
243
  'error.red': 'Could not reach Parallel: {detalle}',
180
244
  'error.servidor': 'Parallel could not answer right now ({detalle}). Try again.',
245
+ 'error.ia': 'Parallel’s AI is temporarily unavailable. Your Actions weren’t charged. Try again in a minute.',
181
246
  'error.demasiado': 'This conversation is too long. Type /new to start another one.',
182
247
  'repl.adios': 'See you.',
183
248
  'repl.registro': 'Log of this session: {ruta}',
@@ -242,6 +307,7 @@ export const TEXTOS = {
242
307
  'tip.bash': 'Runs a shell command right here; parx sees the output',
243
308
  'tip.bandeja': 'Answer what your missions are waiting for',
244
309
  'tip.raton': 'Click options; Shift+drag selects text',
310
+ 'tip.arroba': 'Names one of your apps; a new one opens Connect',
245
311
  };
246
312
  export function cuantos(n, uno, varios, ninguno) {
247
313
  if (n === 0 && ninguno)
@@ -258,17 +324,23 @@ export const COMANDOS = [
258
324
  ['model', 'Swift, Stride or Summit'],
259
325
  ['folder', 'Let parx work in another folder'],
260
326
  ['lanes', 'Run 2 or 3 tasks at once'],
327
+ ['remote', 'Control this parx from your phone'],
261
328
  ['mission', 'Keep it running on Parallel, phone alert when done'],
329
+ ['missions', 'Pick a mission and control it from here'],
262
330
  ['inbox', 'Answer what your missions are waiting for'],
263
331
  ['report', 'Plain summary to paste in email or WhatsApp'],
264
332
  ['teach', 'Explain each step, like a teacher'],
333
+ ['templates', 'Start a mission from a template'],
265
334
  ['undo', 'Undo the last change to a file'],
266
335
  ['new', 'Start a new conversation'],
267
336
  ['balance', 'Your account and your Actions'],
337
+ ['connections', 'Connect your apps (opens the browser)'],
338
+ ['settings', 'Your account and preferences (browser)'],
268
339
  ['notify', 'Desktop notifications: on or off'],
269
340
  ['tips', 'Tips when it opens: on or off'],
270
341
  ['mouse', 'Clicks and wheel in parx: on or off'],
271
342
  ['animation', 'Answers written out as they arrive: on or off'],
343
+ ['autoupdate', 'New versions install themselves: on or off'],
272
344
  ['log', 'Where the log of what it does is kept'],
273
345
  ['exit', 'Leave parx'],
274
346
  ];
@@ -276,12 +348,19 @@ export const ALIAS_COMANDO = {
276
348
  help: 'help', '?': 'help', ayuda: 'help',
277
349
  mode: 'mode', modo: 'mode',
278
350
  model: 'model', modelo: 'model',
351
+ strength: 'model',
279
352
  folder: 'folder', 'add-folder': 'folder', carpeta: 'folder', dir: 'folder',
280
353
  lanes: 'lanes', carriles: 'lanes',
354
+ remote: 'remote', 'remote-control': 'remote', rc: 'remote', remoto: 'remote', phone: 'remote',
281
355
  mission: 'mission', mision: 'mission', misión: 'mission',
356
+ missions: 'missions', misiones: 'missions',
282
357
  inbox: 'inbox', bandeja: 'inbox', approvals: 'inbox', waiting: 'inbox',
358
+ templates: 'templates', template: 'templates', plantillas: 'templates',
359
+ connections: 'connections', conexiones: 'connections', apps: 'connections',
360
+ settings: 'settings', ajustes: 'settings', preferences: 'settings',
283
361
  mouse: 'mouse', raton: 'mouse', ratón: 'mouse',
284
362
  animation: 'animation', animations: 'animation', animacion: 'animation', animación: 'animation',
363
+ autoupdate: 'autoupdate', 'auto-update': 'autoupdate', autoupdates: 'autoupdate', updates: 'autoupdate', autoactualizar: 'autoupdate',
285
364
  report: 'report', informe: 'report',
286
365
  teach: 'teach', class: 'teach', clase: 'teach',
287
366
  undo: 'undo', deshacer: 'undo',
@@ -292,6 +371,7 @@ export const ALIAS_COMANDO = {
292
371
  log: 'log', registro: 'log',
293
372
  exit: 'exit', quit: 'exit', salir: 'exit',
294
373
  };
374
+ export const ALIAS_DESDE = { strength: 2, rc: 2, phone: 3 };
295
375
  export const ALIAS_SI_NO = {
296
376
  on: 'si', yes: 'si', y: 'si', '1': 'si', si: 'si', 'sí': 'si',
297
377
  off: 'no', no: 'no', n: 'no', '0': 'no',
@@ -302,7 +382,7 @@ export const USO = [
302
382
  ' parx "task" do the task and exit',
303
383
  ' parx --lanes "task" "task" ... 2 or 3 tasks at once, each in its own lane',
304
384
  ' parx login | logout | account',
305
- ' parx update update parx to the latest version',
385
+ ' parx update get the latest version now (parx also updates itself; /autoupdate off stops that)',
306
386
  '',
307
387
  'Options:',
308
388
  ' --mode ask|auto|read Ask (default), Auto inside the project, Read only',
@@ -314,6 +394,7 @@ export const USO = [
314
394
  ' --ascii plain characters only (also NO_COLOR for no color)',
315
395
  ' --no-mouse leave the mouse to the terminal (no clicks or wheel in parx)',
316
396
  ' --no-animation show answers whole, without writing them out',
397
+ ' --remote control this session from your phone (same as /remote)',
317
398
  ' --version, --help',
318
399
  '',
319
400
  'Sign in:',
@@ -325,7 +406,15 @@ export const USO = [
325
406
  ' parx logout [--all] sign out here, or on every terminal',
326
407
  ' PARALLEL_API_KEY=par_key_… sign in with that key without saving anything (CI)',
327
408
  '',
409
+ 'Remote control:',
410
+ ' /remote link and QR to control this parx from your phone or a browser',
411
+ ' /remote always turn it on every time parx opens on this computer',
412
+ ' /remote off turn it off here',
413
+ '',
328
414
  'Updates:',
329
- ' parx checks npm for a new version at most once a day, without slowing anything down.',
330
- ' PARX_NO_UPDATE_CHECK=1 turn that check off',
415
+ ' parx checks npm every few hours, without slowing anything down, and installs new versions',
416
+ ' itself: they are verified against npm’s signature and start with your next session.',
417
+ ' /autoupdate off only tell me when there is one (then: parx update)',
418
+ ' PARX_NO_AUTO_UPDATE=1 the same, from the environment',
419
+ ' PARX_NO_UPDATE_CHECK=1 never even check',
331
420
  ].join('\n');
package/dist/tui.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { animacion, c, capacidades, colorDeModo, sep, cabezaDePar, caraDePar, carril, fijarCapacidades, signo, ANCHO_DE_PAR, SIMBOLOS } from "./marca.js";
2
2
  import { anchoDe, envolver, puntos, recortar, rellenar } from "./texto.js";
3
- import { ALIAS_COMANDO, COMANDOS, t as tt } from "./textos.js";
3
+ import { ALIAS_COMANDO, ALIAS_DESDE, COMANDOS, t as tt } from "./textos.js";
4
4
  import { MODELOS, ORDEN_DE_MODELOS } from "./modelos.js";
5
5
  import { consejo, ordenDeConsejos } from "./consejos.js";
6
+ import { conexionesQueSalen, insertarMencion, mencionEnLaCaja, mencionTerminada } from "./menciones.js";
6
7
  export { anchoDe } from "./texto.js";
7
8
  import { conElipsis, fraseDeEspera } from "./actividad.js";
8
9
  export const MARGEN = 2;
@@ -288,9 +289,9 @@ function teclaDeRaton(b, x, y, soltar, botonX10) {
288
289
  }
289
290
  export const RATON_SI = '\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h';
290
291
  export const RATON_NO = '\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l';
291
- 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']);
292
293
  const CON_TEXTO = new Set(['folder']);
293
- 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']);
294
295
  const MODOS_DEL_MENU = ['ask', 'auto', 'read', 'bypass'];
295
296
  export function opcionesDelMenu(valor) {
296
297
  if (!valor.startsWith('/') || valor.includes('\n'))
@@ -301,7 +302,7 @@ export function opcionesDelMenu(valor) {
301
302
  const q = sinBarra.toLocaleLowerCase();
302
303
  const out = [];
303
304
  for (const [cmd, descripcion] of COMANDOS) {
304
- const casa = cmd.startsWith(q) || (q.length > 0 && Object.entries(ALIAS_COMANDO).some(([alias, destino]) => destino === cmd && alias.startsWith(q)));
305
+ const casa = cmd.startsWith(q) || (q.length > 0 && Object.entries(ALIAS_COMANDO).some(([alias, destino]) => destino === cmd && alias.startsWith(q) && q.length >= (ALIAS_DESDE[alias] ?? 1)));
305
306
  if (!casa)
306
307
  continue;
307
308
  const conAlgo = CON_VALOR.has(cmd) || CON_TEXTO.has(cmd);
@@ -321,14 +322,37 @@ export function opcionesDelMenu(valor) {
321
322
  ? MODOS_DEL_MENU.map((k) => [k, tt(`modo.${k}.desc`)])
322
323
  : cmd === 'model'
323
324
  ? ORDEN_DE_MODELOS.map((k) => [k, `${MODELOS[k].nombre} ${MODELOS[k].version}: ${MODELOS[k].descripcion}`])
324
- : [
325
- ['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'],
326
- ['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'],
327
- ];
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
+ ];
328
335
  return valores
329
336
  .filter(([v]) => v.startsWith(q))
330
337
  .map(([v, d]) => ({ nombre: v, descripcion: d, completar: `${base}${v}`, enviar: true, ...(cmd === 'mode' ? { modo: v, ...(v === 'bypass' ? { peligro: true } : {}) } : {}) }));
331
338
  }
339
+ export function opcionesDeArroba(unidades, cursor, conexiones) {
340
+ if (!conexiones || !conexiones.length)
341
+ return null;
342
+ const m = mencionEnLaCaja(unidades, cursor);
343
+ if (!m || mencionTerminada(m.consulta, conexiones))
344
+ return null;
345
+ const salen = conexionesQueSalen(conexiones, m.consulta);
346
+ if (!salen.length)
347
+ return null;
348
+ return salen.map((x) => ({
349
+ nombre: `@${x.nombre}`,
350
+ descripcion: x.conectada ? tt('arroba.conectada') : tt('arroba.conectar'),
351
+ completar: '',
352
+ enviar: false,
353
+ mencion: { inicio: m.inicio, fin: cursor, conexion: x },
354
+ }));
355
+ }
332
356
  export function componerBarra(e, util) {
333
357
  return componerBarraConZonas(e, util).texto;
334
358
  }
@@ -529,7 +553,7 @@ export function componerCarril(d, ancho, alto) {
529
553
  cursor = null;
530
554
  }
531
555
  if (conPar)
532
- filaEscribir += ` ${cabezaDePar()} `;
556
+ filaEscribir += ` ${cabezaDePar(d.gesto)} `;
533
557
  const tinta = (t) => (bash ? c.bash(t) : c.violeta(t));
534
558
  const linea = conPar ? `${tinta(carril(util - ANCHO_DE_PAR - 2))}${caraDePar(d.gesto)}${tinta(carril(2))}` : tinta(carril(util));
535
559
  let barra;
@@ -648,7 +672,8 @@ export function componerCarril(d, ancho, alto) {
648
672
  const nombre = rellenar(recortar(o.nombre, anchoNombre - 1), anchoNombre);
649
673
  if (o.modo)
650
674
  return `${m}${rellenar(`${marca} ${sel || sobre ? c.negrita(tinta(nombre)) : tinta(nombre)}${sel || sobre ? desc : c.tenue(c.gris(desc))}`, util)}`;
651
- return `${m}${rellenar(`${marca} ${sel ? c.negrita(c.marca(nombre)) : sobre ? c.marca(nombre) : c.gris(nombre)}${sel || sobre ? desc : c.tenue(c.gris(desc))}`, util)}`;
675
+ const nota = o.mencion && !o.mencion.conexion.conectada ? c.marca(desc) : sel || sobre ? desc : c.tenue(c.gris(desc));
676
+ return `${m}${rellenar(`${marca} ${sel ? c.negrita(c.marca(nombre)) : sobre ? c.marca(nombre) : c.gris(nombre)}${nota}`, util)}`;
652
677
  });
653
678
  }
654
679
  }
@@ -768,6 +793,8 @@ export class Superficie {
768
793
  borrador = [];
769
794
  menuCerradoPara = null;
770
795
  menuElegida = 0;
796
+ conexiones = null;
797
+ avisadoArroba = false;
771
798
  cola = [];
772
799
  ctrlC = 0;
773
800
  ejemplo = null;
@@ -802,9 +829,51 @@ export class Superficie {
802
829
  escribir(s) {
803
830
  this.o.salida.write(s);
804
831
  }
832
+ menuActual(valor = this.valorCrudo()) {
833
+ if (this.menuCerradoPara === valor)
834
+ return null;
835
+ const barra = opcionesDelMenu(valor);
836
+ if (barra && barra.length)
837
+ return barra;
838
+ const arroba = mencionEnLaCaja(this.unidades, this.cursor);
839
+ if (!arroba) {
840
+ this.avisadoArroba = false;
841
+ return null;
842
+ }
843
+ if (!this.avisadoArroba) {
844
+ this.avisadoArroba = true;
845
+ this.o.alArroba?.();
846
+ }
847
+ return opcionesDeArroba(this.unidades, this.cursor, this.conexiones);
848
+ }
849
+ fijarConexiones(lista) {
850
+ this.conexiones = lista ? [...lista] : null;
851
+ this.pintar();
852
+ }
853
+ ponerEnLaCaja(texto) {
854
+ this.ponerValor(texto);
855
+ this.menuCerradoPara = this.valorCrudo();
856
+ this.menuElegida = 0;
857
+ this.pintar();
858
+ }
859
+ elegirMencion(o) {
860
+ const m = o.mencion;
861
+ if (m.conexion.conectada) {
862
+ const r = insertarMencion(this.unidades, m.inicio, m.fin, m.conexion.nombre);
863
+ this.unidades = r.unidades;
864
+ this.cursor = r.cursor;
865
+ this.menuElegida = 0;
866
+ this.menuCerradoPara = null;
867
+ return;
868
+ }
869
+ this.menuCerradoPara = this.valorCrudo();
870
+ this.menuElegida = 0;
871
+ this.avisadoArroba = false;
872
+ this.o.alConectar?.(m.conexion);
873
+ }
805
874
  datos() {
806
875
  const valor = this.valorCrudo();
807
- const ops = this.menuCerradoPara === valor ? null : opcionesDelMenu(valor);
876
+ const ops = this.menuActual(valor);
808
877
  if (ops && this.menuElegida >= ops.length)
809
878
  this.menuElegida = Math.max(0, ops.length - 1);
810
879
  const pendientes = this.lineas.length - this.volcadas;
@@ -1422,6 +1491,19 @@ export class Superficie {
1422
1491
  this.ponerValor(texto);
1423
1492
  this.enviar();
1424
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
+ }
1425
1507
  get libre() {
1426
1508
  return Boolean(this.resolverLectura) && !this.trabajo && !this.pregunta;
1427
1509
  }
@@ -1457,11 +1539,18 @@ export class Superficie {
1457
1539
  actualizarCarriles() {
1458
1540
  this.pintar();
1459
1541
  }
1460
- preguntar(texto, tipo = 'normal') {
1542
+ preguntar(texto, tipo = 'normal', externo) {
1461
1543
  const o = opcionesDePregunta(tipo);
1462
1544
  const peligro = tipo === 'bypass';
1463
1545
  return new Promise((resolver) => {
1464
- 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({
1465
1554
  texto,
1466
1555
  opciones: o.opciones,
1467
1556
  cortas: o.cortas,
@@ -1469,7 +1558,13 @@ export class Superficie {
1469
1558
  ...(peligro ? { peligro: true } : {}),
1470
1559
  indiceNo: o.valores.indexOf('no'),
1471
1560
  siConY: o.valores[o.elegida] !== 'no' || peligro,
1472
- }, (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
+ });
1473
1568
  });
1474
1569
  }
1475
1570
  elegir(texto, opciones, o = {}) {
@@ -1497,6 +1592,20 @@ export class Superficie {
1497
1592
  this.esperando.push(mostrar);
1498
1593
  else
1499
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
+ };
1500
1609
  }
1501
1610
  responder(i) {
1502
1611
  const p = this.pregunta;
@@ -1613,7 +1722,7 @@ export class Superficie {
1613
1722
  this.aviso = null;
1614
1723
  }
1615
1724
  const valor = this.valorCrudo();
1616
- const ops = this.menuCerradoPara === valor ? null : opcionesDelMenu(valor);
1725
+ const ops = this.menuActual(valor);
1617
1726
  const menu = ops && ops.length ? ops : null;
1618
1727
  switch (k.t) {
1619
1728
  case 'texto':
@@ -1640,6 +1749,10 @@ export class Superficie {
1640
1749
  break;
1641
1750
  }
1642
1751
  case 'intro':
1752
+ if (menu && menu[this.menuElegida]?.mencion) {
1753
+ this.elegirMencion(menu[this.menuElegida]);
1754
+ break;
1755
+ }
1643
1756
  if (menu) {
1644
1757
  const o = menu[this.menuElegida];
1645
1758
  this.ponerValor(o.completar);
@@ -1651,6 +1764,10 @@ export class Superficie {
1651
1764
  this.enviar();
1652
1765
  break;
1653
1766
  case 'tab':
1767
+ if (menu && menu[this.menuElegida]?.mencion) {
1768
+ this.elegirMencion(menu[this.menuElegida]);
1769
+ break;
1770
+ }
1654
1771
  if (menu) {
1655
1772
  this.ponerValor(menu[this.menuElegida].completar);
1656
1773
  this.menuElegida = 0;