@getparallel/parx 0.4.4 → 0.5.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/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ changes). Entries are generated by `npm run release` from the git history of `ap
6
6
 
7
7
  <!-- Notes written under «## Unreleased» are used as the next entry instead of the git log. -->
8
8
 
9
+ ## 0.5.0 (2026-10-18)
10
+
11
+ - `/missions`: your Parallel missions in a list; pick one and open it, pause or resume it (whichever fits its state), share a read-only link (copied), see its result or what the agents did, without leaving parx. `/missions bakery` filters by title.
12
+ - `@` names one of your Parallel apps: the menu lists the connected ones and also the ones you can connect, marked «Connect». Picking a connected app writes `@Gmail ` in the input; picking one that is not connected opens its Connect page in the browser.
13
+ - `/templates` lists the mission templates; the one you pick is written in the input as `/mission <goal>`, for you to review and send.
14
+ - `/connections` and `/settings` open those pages of getparallel.net in the browser (and print the link).
15
+ - `/strength` is the same as `/model` (in parx the strength is the model: Swift, Stride or Summit).
16
+
9
17
  ## 0.4.4 (2026-10-11)
10
18
 
11
19
  - parx always shows what it is doing: «Thinking…» from the first moment, then the real step as it happens («Reading src/app.ts…», «Searching for useAuth…», «Editing 2 files…», «Running tests…», «Writing the answer…»), with the time and the Actions. If the server takes a while, only honest phrases («Planning the steps…», «Looking at the results…», «Waiting for the server…»); the old brand phrases are gone. Same in `--lanes`, at 40 columns, with `NO_COLOR` and `--ascii`.
package/README.md CHANGED
@@ -112,11 +112,14 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
112
112
  | Command | What it does |
113
113
  | --- | --- |
114
114
  | `/mode` | Ask, Auto, Read only or Bypass |
115
- | `/model` | Swift, Stride or Summit |
115
+ | `/model` (or `/strength`) | Swift, Stride or Summit |
116
116
  | `/folder <path>` | let parx work in another folder |
117
117
  | `/lanes` | 2 or 3 tasks at once, each in its own lane, all moving together |
118
118
  | `/mission [goal]` | turn the work into a Parallel mission that keeps running on Parallel’s servers after you close the terminal; phone notification when it is done |
119
+ | `/missions [filter]` | your missions; pick one and open it, pause or resume it, share a read-only link (copied), see its result or its activity, right here |
119
120
  | `/inbox` | what your missions are waiting for (approvals, proposed changes, your steps), as buttons; what you pick takes effect like on the web |
121
+ | `/templates` | pick a mission template; it is written in the input as `/mission <goal>` for you to review and send |
122
+ | `/connections`, `/settings` | open those pages of getparallel.net in the browser (the link is printed too) |
120
123
  | `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
121
124
  | `/teach` | class mode: explains each step as it codes, like a teacher |
122
125
  | `/new` | a clean conversation, as if you had just opened parx: the screen, the terminal scrollback, the queue and what the agent remembers are cleared (mode, model and sign-in stay). If a task is running it asks «Stop and start new?» first |
@@ -126,6 +129,12 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
126
129
  | `/mouse` on, off | clicks and wheel inside parx (saved; `--no-mouse` for one session) |
127
130
  | `/animation` on, off | answers written out as they arrive, or whole (saved; `--no-animation` for one session) |
128
131
 
132
+ **Your apps with `@`:** type `@` and a menu lists your Parallel apps, the connected ones first and
133
+ then the ones you can connect («Connect»); it filters as you type (`@gm`), ↑ ↓ move, Tab or Enter
134
+ pick, Esc closes. A connected app is written in the input (`@Gmail `), so a `/mission` knows which
135
+ of your apps to use; an app that is not connected opens its Connect page in the browser, and the
136
+ next `@` shows it connected.
137
+
129
138
  **Bash mode:** start the input with `!` and it turns amber with a small `bash` label; Enter runs the
130
139
  command in your shell, in the project folder, and its output streams into the conversation with the
131
140
  exit code. The agent sees the command and its output in the next task. Backspace on a lone `!`
package/dist/api.js CHANGED
@@ -88,6 +88,24 @@ export function leerBandeja(servidor, token, señal) {
88
88
  export function responderBandeja(servidor, token, cuerpo, señal) {
89
89
  return llamar(servidor, '/api/terminal/bandeja', { token, cuerpo, señal, plazoMs: 60_000 });
90
90
  }
91
+ export function leerMisiones(servidor, token, señal) {
92
+ return llamar(servidor, '/api/terminal/misiones', { token, señal, plazoMs: 20_000 });
93
+ }
94
+ export function leerMisionDeLaCuenta(servidor, token, id, señal) {
95
+ return llamar(servidor, `/api/terminal/misiones/${encodeURIComponent(id)}`, { token, señal, plazoMs: 20_000 });
96
+ }
97
+ export function cicloDeMision(servidor, token, id, accion, señal) {
98
+ return llamar(servidor, `/api/terminal/misiones/${encodeURIComponent(id)}/ciclo`, { token, cuerpo: { accion }, señal, plazoMs: 60_000 });
99
+ }
100
+ export function compartirMision(servidor, token, id, señal) {
101
+ return llamar(servidor, `/api/terminal/misiones/${encodeURIComponent(id)}/compartir`, { token, cuerpo: {}, señal, plazoMs: 20_000 });
102
+ }
103
+ export function leerConexiones(servidor, token, señal) {
104
+ return llamar(servidor, '/api/terminal/conexiones', { token, señal, plazoMs: 20_000 });
105
+ }
106
+ export function leerPlantillas(servidor, token, señal) {
107
+ return llamar(servidor, '/api/terminal/plantillas', { token, señal, plazoMs: 20_000 });
108
+ }
91
109
  const CORREO_INTERNO = /@cuentas\.getparallel\.net$/i;
92
110
  export function correoVisible(correo) {
93
111
  const c = (correo ?? '').trim();
package/dist/cli.js CHANGED
@@ -9,8 +9,9 @@ import { ALIAS_COMANDO, ALIAS_SI_NO, COMANDOS, t, USO } from "./textos.js";
9
9
  import { c, colorDeModo, fijarCapacidades, sep, signo, SIMBOLOS } from "./marca.js";
10
10
  import { guardarAjustes, leerAjustes, migrarCarpetaVieja, VERSION } from "./config.js";
11
11
  import { MODELO_POR_DEFECTO, MODELOS, modeloDe, nombreDeModelo } from "./modelos.js";
12
- import { correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerCuenta, quienEs, responderBandeja, verMision } from "./api.js";
13
- import { comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
12
+ import { cicloDeMision, compartirMision, correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerConexiones, leerCuenta, leerMisionDeLaCuenta, leerMisiones, leerPlantillas, quienEs, responderBandeja, verMision, } from "./api.js";
13
+ import { abrirNavegador, comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
14
+ import { accionesDeUnaMision, filtrarPorTitulo, hace } from "./comandos.js";
14
15
  import { hacerTarea, mensajeDeError, nuevaSesion } from "./agente.js";
15
16
  import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
16
17
  import { Registro } from "./registro.js";
@@ -92,7 +93,7 @@ export function modosEnColor() {
92
93
  return `${nombres.slice(0, 3).join(', ')} or ${nombres[3]}`;
93
94
  }
94
95
  function ayudaDeComandos() {
95
- return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, k === 'mode' ? modosEnColor() : d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_raton')}` });
96
+ return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, k === 'mode' ? modosEnColor() : d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_arroba')}\n${t('tui.atajos_raton')}` });
96
97
  }
97
98
  async function cuentaOError(cred, pantalla) {
98
99
  try {
@@ -433,6 +434,25 @@ async function principal() {
433
434
  extra: sesion.ensenar ? 'Teach' : undefined,
434
435
  esperando: esperandoEnBandeja ?? 0,
435
436
  });
437
+ let conexionesAl = 0;
438
+ let conexionesViejas = true;
439
+ let sinConexiones = false;
440
+ const cargarConexiones = async () => {
441
+ if (sinConexiones)
442
+ return;
443
+ conexionesAl = Date.now();
444
+ conexionesViejas = false;
445
+ try {
446
+ const r = await leerConexiones(cred.servidor, cred.token);
447
+ superficie?.fijarConexiones(r.conexiones);
448
+ }
449
+ catch (e) {
450
+ if (e instanceof ErrorDeParallel && (e.codigo === 'sin_acceso' || e.codigo === 'sesion'))
451
+ sinConexiones = true;
452
+ else
453
+ conexionesViejas = true;
454
+ }
455
+ };
436
456
  if (interactiva) {
437
457
  superficie = new Superficie({
438
458
  entrada: process.stdin,
@@ -452,6 +472,17 @@ async function principal() {
452
472
  avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
453
473
  },
454
474
  alNuevaEnMarcha: () => void confirmarNuevaEnMarcha(),
475
+ alArroba: () => {
476
+ if (conexionesViejas || Date.now() - conexionesAl > 60_000)
477
+ void cargarConexiones();
478
+ },
479
+ alConectar: (x) => {
480
+ abrirNavegador(x.url);
481
+ registro.apuntar('conectar', { provider: x.provider });
482
+ pantalla.escribir(t('arroba.abriendo', { nombre: x.nombre, url: c.marca(x.url) }));
483
+ pantalla.escribir(c.gris(t('arroba.despues')));
484
+ conexionesViejas = true;
485
+ },
455
486
  });
456
487
  const s = superficie;
457
488
  process.on('exit', () => s.restaurarDeUrgencia());
@@ -504,6 +535,7 @@ async function principal() {
504
535
  const relojBandeja = setInterval(() => void mirarBandeja(), 90_000);
505
536
  relojBandeja.unref();
506
537
  void mirarBandeja();
538
+ void cargarConexiones();
507
539
  }
508
540
  const leerLinea = async (ejemplo = null) => {
509
541
  if (superficie)
@@ -640,6 +672,147 @@ async function principal() {
640
672
  elegida = Math.min(elegida, lista.length - 1);
641
673
  }
642
674
  };
675
+ const pedir = async (texto, fn, errores = {}) => {
676
+ ocupado = true;
677
+ control = new AbortController();
678
+ superficie?.trabajar(() => control.abort());
679
+ pantalla.espera(texto, 'ruta', true);
680
+ try {
681
+ return await fn(control.signal);
682
+ }
683
+ catch (e) {
684
+ if (!(e instanceof ErrorDeParallel))
685
+ throw e;
686
+ const clave = errores[e.codigo === 'servidor' ? e.detalle.slice(0, 3) : e.codigo];
687
+ if (clave)
688
+ pantalla.aviso(t(clave));
689
+ else if (e.codigo !== 'interrumpido')
690
+ pantalla.error(mensajeDeError(e));
691
+ return null;
692
+ }
693
+ finally {
694
+ pantalla.pararEspera();
695
+ ocupado = false;
696
+ superficie?.descansar();
697
+ }
698
+ };
699
+ const notaDeMision = (m) => [t('misiones.progreso', { estado: m.texto || m.estado, n: m.progreso }), m.pendientes ? t('misiones.esperan', { n: m.pendientes }) : ''].filter(Boolean).join(sep());
700
+ const erroresDeMision = { sin_acceso: 'misiones.no_existe', '409': 'misiones.no_se_puede', '429': 'misiones.despacio' };
701
+ const unaMision = async (inicial) => {
702
+ let m = inicial;
703
+ let elegida = 0;
704
+ for (;;) {
705
+ const acciones = accionesDeUnaMision(m.estado);
706
+ const j = await superficie.elegir(m.titulo, [...acciones.map((a) => ({ etiqueta: a.etiqueta, nota: a.nota })), { etiqueta: t('misiones.volver') }], {
707
+ vertical: true,
708
+ detalle: notaDeMision(m),
709
+ elegida,
710
+ });
711
+ if (j === null)
712
+ return 'salir';
713
+ if (j >= acciones.length)
714
+ return 'volver';
715
+ elegida = j;
716
+ const a = acciones[j];
717
+ registro.apuntar('mision_accion', { accion: a.slug });
718
+ if (a.id === 'abrir') {
719
+ abrirNavegador(m.url);
720
+ pantalla.escribir(t('misiones.abierta', { url: c.marca(m.url) }));
721
+ }
722
+ else if (a.id === 'pausar' || a.id === 'reanudar') {
723
+ const accion = a.id;
724
+ const r = await pedir(t('misiones.mirando'), (s) => cicloDeMision(cred.servidor, cred.token, m.id, accion, s), erroresDeMision);
725
+ if (r) {
726
+ pantalla.escribir(`${c.ok(SIMBOLOS.hecho)} ${t(accion === 'pausar' ? 'misiones.pausada' : 'misiones.reanudada', { titulo: m.titulo })}`);
727
+ const otra = await leerMisiones(cred.servidor, cred.token).catch(() => null);
728
+ m = otra?.misiones.find((x) => x.id === m.id) ?? { ...m, estado: r.estado };
729
+ elegida = 0;
730
+ }
731
+ }
732
+ else if (a.id === 'compartir') {
733
+ const r = await pedir(t('misiones.mirando'), (s) => compartirMision(cred.servidor, cred.token, m.id, s), { ...erroresDeMision, '503': 'misiones.sin_compartir' });
734
+ if (r)
735
+ pantalla.escribir(t(copiarAlPortapapeles(r.url) ? 'misiones.compartida' : 'misiones.compartida_sin_copiar', { url: c.marca(r.url) }));
736
+ }
737
+ else {
738
+ const r = await pedir(t('misiones.mirando'), (s) => leerMisionDeLaCuenta(cred.servidor, cred.token, m.id, s), erroresDeMision);
739
+ if (!r)
740
+ continue;
741
+ const d = r.mision;
742
+ const titulo = `${a.id === 'resultado' ? t('misiones.resultado') : t('misiones.actividad')}${sep()}${d.titulo}`;
743
+ if (a.id === 'resultado') {
744
+ const filas = [
745
+ [t('misiones.meta'), d.objetivo ? t('misiones.objetivo', { etiqueta: d.objetivo.etiqueta, valor: d.objetivo.valor.toLocaleString('en-US'), meta: d.objetivo.meta.toLocaleString('en-US') }) : t('misiones.sin_objetivo')],
746
+ [t('misiones.el_plan'), t('misiones.plan', { estado: d.texto || d.estado, n: d.progreso })],
747
+ ];
748
+ pantalla.bloque(bloqueDeTabla(filas, { titulo, pie: t('misiones.ver_resultados', { url: d.resultados }), sangria: 2 }));
749
+ }
750
+ else {
751
+ pantalla.bloque(bloqueDeTabla(d.actividad.map((x) => [hace(x.cuando), x.texto]), { titulo, pie: d.actividad.length ? t('misiones.ver_actividad', { url: d.url }) : t('misiones.sin_actividad'), sangria: 2 }));
752
+ }
753
+ }
754
+ }
755
+ };
756
+ const misiones = async (filtro) => {
757
+ const leer = () => pedir(t('misiones.mirando'), (s) => leerMisiones(cred.servidor, cred.token, s), { sin_acceso: 'misiones.no_disponible', '429': 'misiones.despacio' });
758
+ let r = await leer();
759
+ if (!r)
760
+ return;
761
+ if (!r.misiones.length) {
762
+ pantalla.escribir(t('misiones.nada'));
763
+ return;
764
+ }
765
+ let lista = filtrarPorTitulo(r.misiones, filtro);
766
+ if (!lista.length) {
767
+ pantalla.escribir(t('misiones.sin_coincidencias', { q: filtro }));
768
+ return;
769
+ }
770
+ if (!superficie) {
771
+ for (const m of lista)
772
+ pantalla.escribir(`${m.titulo}${c.gris(`${sep()}${notaDeMision(m)}`)} ${c.gris(m.url)}`);
773
+ return;
774
+ }
775
+ let elegida = 0;
776
+ for (;;) {
777
+ const i = await superficie.elegir(t('misiones.titulo'), lista.map((m) => ({ etiqueta: m.titulo, nota: notaDeMision(m) })), { vertical: true, elegida });
778
+ if (i === null)
779
+ return;
780
+ elegida = i;
781
+ if ((await unaMision(lista[i])) === 'salir')
782
+ return;
783
+ r = await leer();
784
+ if (!r)
785
+ return;
786
+ lista = filtrarPorTitulo(r.misiones, filtro);
787
+ if (!lista.length)
788
+ return;
789
+ elegida = Math.min(elegida, lista.length - 1);
790
+ }
791
+ };
792
+ const plantillas = async (filtro) => {
793
+ const r = await pedir(t('plantillas.mirando'), (s) => leerPlantillas(cred.servidor, cred.token, s), { sin_acceso: 'misiones.no_disponible', '429': 'misiones.despacio' });
794
+ if (!r)
795
+ return;
796
+ if (!r.plantillas.length) {
797
+ pantalla.escribir(t('plantillas.nada'));
798
+ return;
799
+ }
800
+ const lista = filtrarPorTitulo(r.plantillas, filtro);
801
+ if (!lista.length) {
802
+ pantalla.escribir(t('plantillas.sin_coincidencias', { q: filtro }));
803
+ return;
804
+ }
805
+ if (!superficie) {
806
+ for (const p of lista)
807
+ pantalla.escribir(`${p.titulo}${p.resumen ? c.gris(`${sep()}${p.resumen}`) : ''}`);
808
+ return;
809
+ }
810
+ const i = await superficie.elegir(t('plantillas.titulo'), lista.map((p) => ({ etiqueta: p.titulo, nota: p.propia ? t('plantillas.propia') : p.resumen })), { vertical: true });
811
+ if (i === null)
812
+ return;
813
+ superficie.ponerEnLaCaja(`/mission ${lista[i].objetivo.replace(/\s+/g, ' ').trim()}`);
814
+ pantalla.escribir(c.gris(t('plantillas.lista')));
815
+ };
643
816
  const bash = async (comando) => {
644
817
  if (!comando)
645
818
  return;
@@ -846,6 +1019,23 @@ async function principal() {
846
1019
  await bandeja();
847
1020
  superficie?.pintar();
848
1021
  break;
1022
+ case 'missions':
1023
+ await misiones(resto);
1024
+ superficie?.pintar();
1025
+ break;
1026
+ case 'templates':
1027
+ await plantillas(resto);
1028
+ superficie?.pintar();
1029
+ break;
1030
+ case 'connections':
1031
+ case 'settings': {
1032
+ const url = `${cred.servidor.replace(/\/$/, '')}/${cmd}`;
1033
+ abrirNavegador(url);
1034
+ if (cmd === 'connections')
1035
+ conexionesViejas = true;
1036
+ pantalla.escribir(t('ir.abriendo', { que: t(cmd === 'connections' ? 'ir.conexiones' : 'ir.ajustes'), url: c.marca(url) }));
1037
+ break;
1038
+ }
849
1039
  case 'mouse': {
850
1040
  const si = ALIAS_SI_NO[arg] ?? (superficie?.conRaton ? 'no' : 'si');
851
1041
  guardarAjustes({ raton: si === 'si' });
@@ -0,0 +1,71 @@
1
+ export const DE_LA_WEB = {
2
+ plantillas: { slug: 'templates', parx: 'templates', como: 'nuevo', porque: 'Pick one and it is written as /mission <goal> in the input, to review and send.' },
3
+ adjuntar: { slug: 'attach', parx: null, como: 'no', porque: 'parx already reads the files of your project; images go in by dragging them into the window.' },
4
+ fuerza: { slug: 'strength', parx: 'model', como: 'alias', porque: 'In parx the strength is the model: Swift, Stride or Summit.' },
5
+ dictar: { slug: 'dictate', parx: null, como: 'no', porque: 'A terminal has no microphone of its own; the dictation of your system still types into it.' },
6
+ nueva: { slug: 'new', parx: 'new', como: 'choque', porque: 'parx keeps /new as a new conversation; a new mission is /mission.' },
7
+ misiones: { slug: 'missions', parx: 'missions', como: 'nuevo', porque: 'Your missions; pick one to open, pause or resume, share, see its result or its activity.' },
8
+ conexiones: { slug: 'connections', parx: 'connections', como: 'nuevo', porque: 'Opens your connections on getparallel.net.' },
9
+ ajustes: { slug: 'settings', parx: 'settings', como: 'nuevo', porque: 'Opens your settings on getparallel.net.' },
10
+ abrir: { slug: 'open', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
11
+ pausar: { slug: 'pause', parx: null, como: 'en_misiones', porque: 'Inside /missions, only for a running mission.' },
12
+ reanudar: { slug: 'resume', parx: null, como: 'en_misiones', porque: 'Inside /missions, only for a paused mission.' },
13
+ compartir: { slug: 'share', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
14
+ resultado: { slug: 'result', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
15
+ actividad: { slug: 'activity', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
16
+ ayuda: { slug: 'help', parx: 'help', como: 'ya', porque: 'parx already had /help.' },
17
+ };
18
+ export const ACCIONES_DE_MISION = [
19
+ { id: 'abrir', slug: 'open', etiqueta: 'Open', nota: 'In the browser' },
20
+ { id: 'pausar', slug: 'pause', etiqueta: 'Pause', nota: 'Stop the agents' },
21
+ { id: 'reanudar', slug: 'resume', etiqueta: 'Resume', nota: 'Pick up where it was' },
22
+ { id: 'compartir', slug: 'share', etiqueta: 'Share', nota: 'Read-only link, copied' },
23
+ { id: 'resultado', slug: 'result', etiqueta: 'Result', nota: 'How the goal is going' },
24
+ { id: 'actividad', slug: 'activity', etiqueta: 'Activity', nota: 'What the agents did' },
25
+ ];
26
+ export function accionDeMisionPosible(id, estado) {
27
+ if (id === 'pausar')
28
+ return estado === 'active' || estado === 'at_risk';
29
+ if (id === 'reanudar')
30
+ return estado === 'paused';
31
+ return true;
32
+ }
33
+ export function accionesDeUnaMision(estado) {
34
+ return ACCIONES_DE_MISION.filter((a) => accionDeMisionPosible(a.id, estado));
35
+ }
36
+ export function normalizar(texto) {
37
+ return texto.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
38
+ }
39
+ export function filtrarPorTitulo(lista, consulta) {
40
+ const q = normalizar(consulta.trim());
41
+ if (!q)
42
+ return [...lista];
43
+ const puntuados = [];
44
+ lista.forEach((x, i) => {
45
+ const titulo = normalizar(x.titulo);
46
+ let p = -1;
47
+ if (titulo.startsWith(q))
48
+ p = 0;
49
+ else if (titulo.split(/[\s._-]+/).some((w) => w.startsWith(q)))
50
+ p = 1;
51
+ else if (titulo.includes(q))
52
+ p = 2;
53
+ if (p >= 0)
54
+ puntuados.push({ x, p, i });
55
+ });
56
+ return puntuados.sort((a, b) => a.p - b.p || a.i - b.i).map((v) => v.x);
57
+ }
58
+ export function hace(iso, ahora = Date.now()) {
59
+ const ms = ahora - new Date(iso).getTime();
60
+ if (!Number.isFinite(ms))
61
+ return '';
62
+ const min = Math.floor(ms / 60_000);
63
+ if (min < 1)
64
+ return 'just now';
65
+ if (min < 60)
66
+ return `${min}m ago`;
67
+ const h = Math.floor(min / 60);
68
+ if (h < 24)
69
+ return `${h}h ago`;
70
+ return `${Math.floor(h / 24)}d ago`;
71
+ }
package/dist/consejos.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { capacidades } from "./marca.js";
2
2
  import { t } from "./textos.js";
3
- export const CONSEJOS = ['menu', 'modo', 'una', 'bash', 'imagen', 'historial', 'pegar', 'deshacer', 'modelo', 'carriles', 'parar', 'cola', 'bandeja', 'raton', 'informe', 'mision', 'ensenar', 'saldo', 'cuenta', 'agents'];
3
+ export const CONSEJOS = ['menu', 'modo', 'una', 'bash', 'imagen', 'historial', 'pegar', 'deshacer', 'modelo', 'carriles', 'parar', 'cola', 'bandeja', 'raton', 'informe', 'mision', 'ensenar', 'saldo', 'cuenta', 'agents', 'arroba'];
4
4
  export function consejo(id) {
5
5
  const u = capacidades().unicode;
6
6
  const tecla = {
@@ -24,6 +24,7 @@ export function consejo(id) {
24
24
  bash: '!',
25
25
  bandeja: '/inbox',
26
26
  raton: 'Click',
27
+ arroba: '@',
27
28
  };
28
29
  return { tecla: tecla[id], texto: t(`tip.${id}`) };
29
30
  }
@@ -0,0 +1,85 @@
1
+ export const LARGO_MAXIMO_CONSULTA = 30;
2
+ export function normalizar(texto) {
3
+ return texto.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
4
+ }
5
+ export function detectarArroba(texto, cursor) {
6
+ if (cursor < 1 || cursor > texto.length)
7
+ return null;
8
+ const antes = texto.slice(0, cursor);
9
+ const arroba = antes.lastIndexOf('@');
10
+ if (arroba < 0)
11
+ return null;
12
+ const previo = arroba === 0 ? '' : antes[arroba - 1];
13
+ if (previo !== '' && !/\s/.test(previo))
14
+ return null;
15
+ const consulta = antes.slice(arroba + 1);
16
+ if (consulta.length > LARGO_MAXIMO_CONSULTA)
17
+ return null;
18
+ if (/\n|\s\s|^\s/.test(consulta))
19
+ return null;
20
+ return { inicio: arroba, consulta };
21
+ }
22
+ export function mencionTerminada(consulta, conexiones) {
23
+ if (!/\s$/.test(consulta))
24
+ return false;
25
+ const q = normalizar(consulta.trim());
26
+ return conexiones.some((c) => normalizar(c.nombre) === q || normalizar(c.provider) === q);
27
+ }
28
+ export function filtrarConexiones(conexiones, consulta) {
29
+ return puntuar(conexiones, consulta)
30
+ .sort((a, b) => a.p - b.p || a.c.nombre.localeCompare(b.c.nombre))
31
+ .map((x) => x.c);
32
+ }
33
+ function puntuar(conexiones, consulta) {
34
+ const q = normalizar(consulta.trim());
35
+ const puntuados = [];
36
+ for (const c of conexiones) {
37
+ const nombre = normalizar(c.nombre);
38
+ const clave = normalizar(c.provider);
39
+ let p = -1;
40
+ if (!q || nombre.startsWith(q) || clave.startsWith(q))
41
+ p = 0;
42
+ else if (nombre.split(/[\s._-]+/).some((w) => w.startsWith(q)) || clave.split('_').some((w) => w.startsWith(q)))
43
+ p = 1;
44
+ else if (nombre.includes(q) || clave.includes(q))
45
+ p = 2;
46
+ if (p >= 0)
47
+ puntuados.push({ c, p });
48
+ }
49
+ return puntuados;
50
+ }
51
+ export function conexionesQueSalen(conexiones, consulta, max = 40) {
52
+ return puntuar(conexiones, consulta)
53
+ .sort((a, b) => a.p - b.p || Number(b.c.conectada) - Number(a.c.conectada) || a.c.nombre.localeCompare(b.c.nombre))
54
+ .slice(0, max)
55
+ .map((x) => x.c);
56
+ }
57
+ export function mencionEnLaCaja(unidades, cursor) {
58
+ if (unidades[0] === '!')
59
+ return null;
60
+ const antes = unidades.slice(0, Math.max(0, Math.min(cursor, unidades.length)));
61
+ let arroba = -1;
62
+ for (let i = antes.length - 1; i >= 0; i--) {
63
+ const u = antes[i];
64
+ if (u === '@') {
65
+ arroba = i;
66
+ break;
67
+ }
68
+ if (u.length !== 1 || antes.length - i > LARGO_MAXIMO_CONSULTA + 1)
69
+ return null;
70
+ }
71
+ if (arroba < 0)
72
+ return null;
73
+ const previo = arroba === 0 ? '' : antes[arroba - 1];
74
+ if (previo !== '' && (previo.length !== 1 || !/\s/.test(previo)))
75
+ return null;
76
+ const consulta = antes.slice(arroba + 1).join('');
77
+ return detectarArroba(`@${consulta}`, consulta.length + 1) ? { inicio: arroba, consulta } : null;
78
+ }
79
+ export function insertarMencion(unidades, inicio, cursor, nombre) {
80
+ const despues = unidades.slice(cursor);
81
+ const separador = despues[0] === ' ' ? [] : [' '];
82
+ const etiqueta = [...Array.from(`@${nombre}`), ...separador];
83
+ const nuevas = [...unidades.slice(0, inicio), ...etiqueta, ...despues];
84
+ return { unidades: nuevas, cursor: inicio + etiqueta.length + (separador.length ? 0 : 1) };
85
+ }
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',
@@ -242,6 +283,7 @@ export const TEXTOS = {
242
283
  'tip.bash': 'Runs a shell command right here; parx sees the output',
243
284
  'tip.bandeja': 'Answer what your missions are waiting for',
244
285
  'tip.raton': 'Click options; Shift+drag selects text',
286
+ 'tip.arroba': 'Names one of your apps; a new one opens Connect',
245
287
  };
246
288
  export function cuantos(n, uno, varios, ninguno) {
247
289
  if (n === 0 && ninguno)
@@ -259,12 +301,16 @@ export const COMANDOS = [
259
301
  ['folder', 'Let parx work in another folder'],
260
302
  ['lanes', 'Run 2 or 3 tasks at once'],
261
303
  ['mission', 'Keep it running on Parallel, phone alert when done'],
304
+ ['missions', 'Pick a mission and control it from here'],
262
305
  ['inbox', 'Answer what your missions are waiting for'],
263
306
  ['report', 'Plain summary to paste in email or WhatsApp'],
264
307
  ['teach', 'Explain each step, like a teacher'],
308
+ ['templates', 'Start a mission from a template'],
265
309
  ['undo', 'Undo the last change to a file'],
266
310
  ['new', 'Start a new conversation'],
267
311
  ['balance', 'Your account and your Actions'],
312
+ ['connections', 'Connect your apps (opens the browser)'],
313
+ ['settings', 'Your account and preferences (browser)'],
268
314
  ['notify', 'Desktop notifications: on or off'],
269
315
  ['tips', 'Tips when it opens: on or off'],
270
316
  ['mouse', 'Clicks and wheel in parx: on or off'],
@@ -276,10 +322,15 @@ export const ALIAS_COMANDO = {
276
322
  help: 'help', '?': 'help', ayuda: 'help',
277
323
  mode: 'mode', modo: 'mode',
278
324
  model: 'model', modelo: 'model',
325
+ strength: 'model',
279
326
  folder: 'folder', 'add-folder': 'folder', carpeta: 'folder', dir: 'folder',
280
327
  lanes: 'lanes', carriles: 'lanes',
281
328
  mission: 'mission', mision: 'mission', misión: 'mission',
329
+ missions: 'missions', misiones: 'missions',
282
330
  inbox: 'inbox', bandeja: 'inbox', approvals: 'inbox', waiting: 'inbox',
331
+ templates: 'templates', template: 'templates', plantillas: 'templates',
332
+ connections: 'connections', conexiones: 'connections', apps: 'connections',
333
+ settings: 'settings', ajustes: 'settings', preferences: 'settings',
283
334
  mouse: 'mouse', raton: 'mouse', ratón: 'mouse',
284
335
  animation: 'animation', animations: 'animation', animacion: 'animation', animación: 'animation',
285
336
  report: 'report', informe: 'report',
@@ -292,6 +343,7 @@ export const ALIAS_COMANDO = {
292
343
  log: 'log', registro: 'log',
293
344
  exit: 'exit', quit: 'exit', salir: 'exit',
294
345
  };
346
+ export const ALIAS_DESDE = { strength: 2 };
295
347
  export const ALIAS_SI_NO = {
296
348
  on: 'si', yes: 'si', y: 'si', '1': 'si', si: 'si', 'sí': 'si',
297
349
  off: 'no', no: 'no', n: 'no', '0': 'no',
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;
@@ -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);
@@ -329,6 +330,23 @@ export function opcionesDelMenu(valor) {
329
330
  .filter(([v]) => v.startsWith(q))
330
331
  .map(([v, d]) => ({ nombre: v, descripcion: d, completar: `${base}${v}`, enviar: true, ...(cmd === 'mode' ? { modo: v, ...(v === 'bypass' ? { peligro: true } : {}) } : {}) }));
331
332
  }
333
+ export function opcionesDeArroba(unidades, cursor, conexiones) {
334
+ if (!conexiones || !conexiones.length)
335
+ return null;
336
+ const m = mencionEnLaCaja(unidades, cursor);
337
+ if (!m || mencionTerminada(m.consulta, conexiones))
338
+ return null;
339
+ const salen = conexionesQueSalen(conexiones, m.consulta);
340
+ if (!salen.length)
341
+ return null;
342
+ return salen.map((x) => ({
343
+ nombre: `@${x.nombre}`,
344
+ descripcion: x.conectada ? tt('arroba.conectada') : tt('arroba.conectar'),
345
+ completar: '',
346
+ enviar: false,
347
+ mencion: { inicio: m.inicio, fin: cursor, conexion: x },
348
+ }));
349
+ }
332
350
  export function componerBarra(e, util) {
333
351
  return componerBarraConZonas(e, util).texto;
334
352
  }
@@ -648,7 +666,8 @@ export function componerCarril(d, ancho, alto) {
648
666
  const nombre = rellenar(recortar(o.nombre, anchoNombre - 1), anchoNombre);
649
667
  if (o.modo)
650
668
  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)}`;
669
+ const nota = o.mencion && !o.mencion.conexion.conectada ? c.marca(desc) : sel || sobre ? desc : c.tenue(c.gris(desc));
670
+ return `${m}${rellenar(`${marca} ${sel ? c.negrita(c.marca(nombre)) : sobre ? c.marca(nombre) : c.gris(nombre)}${nota}`, util)}`;
652
671
  });
653
672
  }
654
673
  }
@@ -768,6 +787,8 @@ export class Superficie {
768
787
  borrador = [];
769
788
  menuCerradoPara = null;
770
789
  menuElegida = 0;
790
+ conexiones = null;
791
+ avisadoArroba = false;
771
792
  cola = [];
772
793
  ctrlC = 0;
773
794
  ejemplo = null;
@@ -802,9 +823,51 @@ export class Superficie {
802
823
  escribir(s) {
803
824
  this.o.salida.write(s);
804
825
  }
826
+ menuActual(valor = this.valorCrudo()) {
827
+ if (this.menuCerradoPara === valor)
828
+ return null;
829
+ const barra = opcionesDelMenu(valor);
830
+ if (barra && barra.length)
831
+ return barra;
832
+ const arroba = mencionEnLaCaja(this.unidades, this.cursor);
833
+ if (!arroba) {
834
+ this.avisadoArroba = false;
835
+ return null;
836
+ }
837
+ if (!this.avisadoArroba) {
838
+ this.avisadoArroba = true;
839
+ this.o.alArroba?.();
840
+ }
841
+ return opcionesDeArroba(this.unidades, this.cursor, this.conexiones);
842
+ }
843
+ fijarConexiones(lista) {
844
+ this.conexiones = lista ? [...lista] : null;
845
+ this.pintar();
846
+ }
847
+ ponerEnLaCaja(texto) {
848
+ this.ponerValor(texto);
849
+ this.menuCerradoPara = this.valorCrudo();
850
+ this.menuElegida = 0;
851
+ this.pintar();
852
+ }
853
+ elegirMencion(o) {
854
+ const m = o.mencion;
855
+ if (m.conexion.conectada) {
856
+ const r = insertarMencion(this.unidades, m.inicio, m.fin, m.conexion.nombre);
857
+ this.unidades = r.unidades;
858
+ this.cursor = r.cursor;
859
+ this.menuElegida = 0;
860
+ this.menuCerradoPara = null;
861
+ return;
862
+ }
863
+ this.menuCerradoPara = this.valorCrudo();
864
+ this.menuElegida = 0;
865
+ this.avisadoArroba = false;
866
+ this.o.alConectar?.(m.conexion);
867
+ }
805
868
  datos() {
806
869
  const valor = this.valorCrudo();
807
- const ops = this.menuCerradoPara === valor ? null : opcionesDelMenu(valor);
870
+ const ops = this.menuActual(valor);
808
871
  if (ops && this.menuElegida >= ops.length)
809
872
  this.menuElegida = Math.max(0, ops.length - 1);
810
873
  const pendientes = this.lineas.length - this.volcadas;
@@ -1613,7 +1676,7 @@ export class Superficie {
1613
1676
  this.aviso = null;
1614
1677
  }
1615
1678
  const valor = this.valorCrudo();
1616
- const ops = this.menuCerradoPara === valor ? null : opcionesDelMenu(valor);
1679
+ const ops = this.menuActual(valor);
1617
1680
  const menu = ops && ops.length ? ops : null;
1618
1681
  switch (k.t) {
1619
1682
  case 'texto':
@@ -1640,6 +1703,10 @@ export class Superficie {
1640
1703
  break;
1641
1704
  }
1642
1705
  case 'intro':
1706
+ if (menu && menu[this.menuElegida]?.mencion) {
1707
+ this.elegirMencion(menu[this.menuElegida]);
1708
+ break;
1709
+ }
1643
1710
  if (menu) {
1644
1711
  const o = menu[this.menuElegida];
1645
1712
  this.ponerValor(o.completar);
@@ -1651,6 +1718,10 @@ export class Superficie {
1651
1718
  this.enviar();
1652
1719
  break;
1653
1720
  case 'tab':
1721
+ if (menu && menu[this.menuElegida]?.mencion) {
1722
+ this.elegirMencion(menu[this.menuElegida]);
1723
+ break;
1724
+ }
1654
1725
  if (menu) {
1655
1726
  this.ponerValor(menu[this.menuElegida].completar);
1656
1727
  this.menuElegida = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getparallel/parx",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "parx: the coding agent of Parallel in your terminal. Reads and edits your project, runs commands and tests, and does coding tasks end to end. Mac, Windows and Linux.",
5
5
  "keywords": [
6
6
  "parx",