@getparallel/parx 0.4.3 → 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,21 @@ 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
+
17
+ ## 0.4.4 (2026-10-11)
18
+
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`.
20
+ - The answer is written out word by word with a soft cursor, in 2 seconds at most, and ends exactly as before (markdown, code blocks, wrapping). Only the rows that change are repainted; scrolling up while it writes does not jump. Any key shows the rest at once; `/animation off` (saved) or `--no-animation` shows answers whole, and so do pipes, CI, `--lanes` and `/report`.
21
+ - `/new` starts truly fresh: the screen and the terminal scrollback are cleared in one go, the queue, the waiting lines and what the agent remembers are gone, and you see the welcome and the tips again, like when parx opens. Mode, model and sign-in stay. If a task is running, it asks «Stop and start new?» with buttons first.
22
+ - `/mouse off` is remembered in the next session too (it was saved but not read back).
23
+
9
24
  ## 0.4.3 (2026-10-11)
10
25
 
11
26
  - Each permission mode has its own colour, from least to most freedom: Read only teal, Ask violet, Auto orange, Bypass red. It shows in the status row, the `/mode` menu, the `/mode` confirmation, `/help` and the `╱╱` input marker; bash mode keeps its amber.
package/README.md CHANGED
@@ -93,27 +93,47 @@ when it asks first:
93
93
  ## Inside a session
94
94
 
95
95
  The input sits at the bottom from the start; the status lives in one place, the row under the rail:
96
- the mode and the model at rest; while it works, what it is doing in plain words («Reading 3 files»,
97
- «Running tests», or «Tracing the route…» while it plans), the time and the Actions so far, with a
98
- small rail animation (a dot along the route, a train on the track, a sleeper scanning the rails, the
99
- track being laid).
96
+ the mode and the model at rest; while it works, it always says what it is doing right now, with a
97
+ small rail animation in violet, the time and the Actions so far. It is never silent: «Thinking…»
98
+ while Parallel thinks, then the real step as it happens («Reading src/app.ts…», «Searching for
99
+ useAuth…», «Editing 2 files…», «Running tests…», «Running git status…», «Writing the answer…»). If
100
+ the server takes a while, only what is really going on («Planning the steps…», «Looking at the
101
+ results…», «Waiting for the server…»). It works the same in `--lanes` (one row per lane), at 40
102
+ columns, with `NO_COLOR` and with `--ascii`.
103
+
104
+ **The answer is written out**, word by word, with a soft cursor, in 2 seconds at most (long answers
105
+ go faster). It is laid out whole first, so markdown and code blocks end exactly as if they had
106
+ appeared at once, and scrolling up while it writes does not move what you are reading. Any key shows
107
+ the rest at once. `/animation off` (saved) or `--no-animation` shows answers whole; without a
108
+ terminal (a pipe, CI), in `--lanes` and in `/report` they are always whole.
100
109
 
101
110
  Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or Enter pick, Esc closes.
102
111
 
103
112
  | Command | What it does |
104
113
  | --- | --- |
105
114
  | `/mode` | Ask, Auto, Read only or Bypass |
106
- | `/model` | Swift, Stride or Summit |
115
+ | `/model` (or `/strength`) | Swift, Stride or Summit |
107
116
  | `/folder <path>` | let parx work in another folder |
108
117
  | `/lanes` | 2 or 3 tasks at once, each in its own lane, all moving together |
109
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 |
110
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) |
111
123
  | `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
112
124
  | `/teach` | class mode: explains each step as it codes, like a teacher |
113
- | `/undo`, `/new`, `/balance`, `/log`, `/help`, `/exit` | the usual |
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
+ | `/undo`, `/balance`, `/log`, `/help`, `/exit` | the usual |
114
127
  | `/notify` on, off | desktop notifications |
115
128
  | `/tips` on, off | the welcome tips |
116
129
  | `/mouse` on, off | clicks and wheel inside parx (saved; `--no-mouse` for one session) |
130
+ | `/animation` on, off | answers written out as they arrive, or whole (saved; `--no-animation` for one session) |
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.
117
137
 
118
138
  **Bash mode:** start the input with `!` and it turns amber with a small `bash` label; Enter runs the
119
139
  command in your shell, in the project folder, and its output streams into the conversation with the
@@ -0,0 +1,131 @@
1
+ import { capacidades, c } from "./marca.js";
2
+ import { t } from "./textos.js";
3
+ import { anchoDeCaracter, sinColor } from "./texto.js";
4
+ export function elipsis() {
5
+ return capacidades().unicode ? '…' : '...';
6
+ }
7
+ export function conElipsis(texto) {
8
+ const s = texto.trimEnd();
9
+ if (!s || /(…|\.\.\.|[.!?:])$/.test(s))
10
+ return s;
11
+ return `${s}${elipsis()}`;
12
+ }
13
+ const AL_EMPEZAR = [
14
+ [0, 'trabajo.pensando'],
15
+ [6_000, 'trabajo.planear'],
16
+ [15_000, 'trabajo.sigue'],
17
+ [35_000, 'trabajo.servidor'],
18
+ ];
19
+ const TRAS_HERRAMIENTAS = [
20
+ [0, 'trabajo.pensando'],
21
+ [5_000, 'trabajo.resultados'],
22
+ [11_000, 'trabajo.siguiente'],
23
+ [20_000, 'trabajo.sigue'],
24
+ [40_000, 'trabajo.servidor'],
25
+ ];
26
+ export function fraseDeEspera(ms, trasHerramientas = false) {
27
+ const tabla = trasHerramientas ? TRAS_HERRAMIENTAS : AL_EMPEZAR;
28
+ let clave = tabla[0][1];
29
+ for (const [desde, k] of tabla)
30
+ if (Math.max(0, ms) >= desde)
31
+ clave = k;
32
+ return conElipsis(t(clave));
33
+ }
34
+ export const CARACTERES_POR_SEGUNDO = 110;
35
+ export const MS_MAX_RESPUESTA = 2000;
36
+ export const MS_MAX_INTERMEDIO = 900;
37
+ export const MS_POR_FOTOGRAMA = 33;
38
+ export function duracionDeRevelado(caracteres, maxMs = MS_MAX_RESPUESTA) {
39
+ if (caracteres <= 0)
40
+ return 0;
41
+ return Math.round(Math.min(maxMs, Math.max(160, (caracteres / CARACTERES_POR_SEGUNDO) * 1000)));
42
+ }
43
+ export function caracteresVisibles(total, ms, duracion) {
44
+ if (duracion <= 0 || ms >= duracion)
45
+ return total;
46
+ return Math.max(0, Math.min(total, Math.floor((total * Math.max(0, ms)) / duracion)));
47
+ }
48
+ export function cursorEncendido(ms) {
49
+ return Math.floor(Math.max(0, ms) / 500) % 2 === 0;
50
+ }
51
+ export function simboloDeCursor() {
52
+ return c.violeta(capacidades().unicode ? '▍' : '_');
53
+ }
54
+ export function caracteresDe(lineas) {
55
+ let n = 0;
56
+ for (const l of lineas)
57
+ n += anchoVisible(l);
58
+ return n;
59
+ }
60
+ function anchoVisible(l) {
61
+ let n = 0;
62
+ for (const ch of sinColor(l))
63
+ n += anchoDeCaracter(ch);
64
+ return n;
65
+ }
66
+ const ESCAPE = /\x1b\[[0-9;?]*[ -/]*[@-~]/y;
67
+ export function cortarVisible(linea, n) {
68
+ if (n <= 0)
69
+ return '';
70
+ let out = '';
71
+ let w = 0;
72
+ let color = false;
73
+ let i = 0;
74
+ while (i < linea.length) {
75
+ if (linea[i] === '\x1b') {
76
+ ESCAPE.lastIndex = i;
77
+ const m = ESCAPE.exec(linea);
78
+ if (m) {
79
+ out += m[0];
80
+ color = true;
81
+ i += m[0].length;
82
+ continue;
83
+ }
84
+ }
85
+ const ch = String.fromCodePoint(linea.codePointAt(i));
86
+ const cw = anchoDeCaracter(ch);
87
+ if (w + cw > n)
88
+ return `${out}${color ? '\x1b[0m' : ''}`;
89
+ out += ch;
90
+ w += cw;
91
+ i += ch.length;
92
+ }
93
+ return out;
94
+ }
95
+ export function revelarLineas(lineas, n, cursor, ancho = Infinity) {
96
+ const total = caracteresDe(lineas);
97
+ if (n >= total)
98
+ return cursor ? conCursor([...lineas], cursor, ancho) : [...lineas];
99
+ const out = [];
100
+ let quedan = Math.max(0, n);
101
+ for (const l of lineas) {
102
+ const w = anchoVisible(l);
103
+ if (quedan >= w) {
104
+ out.push(l);
105
+ quedan -= w;
106
+ continue;
107
+ }
108
+ const plano = Array.from(sinColor(l));
109
+ let cols = 0;
110
+ let k = 0;
111
+ while (k < plano.length && cols < quedan)
112
+ cols += anchoDeCaracter(plano[k++]);
113
+ while (k < plano.length && plano[k] !== ' ')
114
+ cols += anchoDeCaracter(plano[k++]);
115
+ const parte = cortarVisible(l, cols);
116
+ out.push(sinColor(parte).trim() ? parte : '');
117
+ break;
118
+ }
119
+ while (out.length > 1 && out[out.length - 1] === '' && lineas[out.length - 1] !== '')
120
+ out.pop();
121
+ return cursor ? conCursor(out, cursor, ancho) : out;
122
+ }
123
+ function conCursor(lineas, cursor, ancho) {
124
+ if (!lineas.length)
125
+ return [cursor];
126
+ const i = lineas.length - 1;
127
+ const l = lineas[i];
128
+ if (anchoVisible(l) + 1 < ancho)
129
+ lineas[i] = `${l.replace(/\s+$/, '')}${anchoVisible(l) ? ' ' : ''}${cursor}`;
130
+ return lineas;
131
+ }
package/dist/agente.js CHANGED
@@ -153,29 +153,31 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
153
153
  disponibles = r.acciones.disponibles;
154
154
  pantalla.progreso(acciones);
155
155
  registro.apuntar('turno', { tarea, vuelta, acciones });
156
- const llamadas = [];
156
+ const llamadas = r.salida.filter((i) => i.type === 'function_call');
157
157
  for (const item of r.salida) {
158
158
  s.conversacion.push(item);
159
159
  if (item.type === 'message') {
160
160
  const texto = textoDeMensaje(item).trim();
161
161
  if (texto) {
162
162
  respuesta = texto;
163
+ pantalla.espera(t('hace.respuesta'), 'tendido', true);
163
164
  pantalla.escribir();
164
- pantalla.texto(texto);
165
+ await pantalla.textoAnimado(texto, { intermedio: llamadas.length > 0 });
165
166
  pantalla.escribir();
167
+ pantalla.espera(null);
166
168
  }
167
169
  }
168
- else if (item.type === 'function_call')
169
- llamadas.push(item);
170
170
  }
171
171
  if (!llamadas.length)
172
172
  break;
173
- const etiqueta = etiquetaDeTanda(llamadas.map((l) => ({ name: String(l.name ?? ''), arguments: String(l.arguments ?? '{}') })));
174
- if (etiqueta)
175
- pantalla.espera(etiqueta.texto, etiqueta.anim, false);
176
- for (const ll of llamadas) {
173
+ const todas = llamadas.map((l) => ({ name: String(l.name ?? ''), arguments: String(l.arguments ?? '{}') }));
174
+ for (let i = 0; i < llamadas.length; i++) {
175
+ const ll = llamadas[i];
177
176
  if (señal.aborted)
178
177
  throw new ErrorDeParallel('interrumpido');
178
+ const etiqueta = etiquetaDeTanda(grupoDesde(todas, i));
179
+ if (etiqueta && empiezaGrupo(todas, i))
180
+ pantalla.espera(etiqueta.texto, etiqueta.anim, false);
179
181
  const salida = await ejecutarHerramienta(String(ll.name ?? ''), String(ll.arguments ?? '{}'), s.ctx);
180
182
  s.conversacion.push({ type: 'function_call_output', call_id: ll.call_id, output: salida });
181
183
  }
@@ -209,6 +211,31 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
209
211
  pantalla.cierre(`${t('estado.hecho')}${sep}${c.gris(textoDeAcciones(acciones))}${sep}${c.gris(duracion(ms))}`);
210
212
  return true;
211
213
  }
214
+ function familia(nombre) {
215
+ if (nombre === 'write_file' || nombre === 'edit_file')
216
+ return 'cambio';
217
+ if (nombre === 'read_file' || nombre === 'search' || nombre === 'list_files')
218
+ return nombre;
219
+ return null;
220
+ }
221
+ export function grupoDesde(llamadas, i) {
222
+ const actual = llamadas[i];
223
+ if (!actual)
224
+ return [];
225
+ const f = familia(actual.name);
226
+ if (!f)
227
+ return [actual];
228
+ let j = i;
229
+ while (j < llamadas.length && familia(llamadas[j].name) === f)
230
+ j++;
231
+ return llamadas.slice(i, j);
232
+ }
233
+ export function empiezaGrupo(llamadas, i) {
234
+ if (i === 0)
235
+ return true;
236
+ const f = familia(llamadas[i].name);
237
+ return !f || f !== familia(llamadas[i - 1].name);
238
+ }
212
239
  function cerrarLlamadasPendientes(items) {
213
240
  const respondidas = new Set(items.filter((i) => i.type === 'function_call_output').map((i) => i.call_id));
214
241
  for (const i of [...items]) {
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";
@@ -43,6 +44,8 @@ function opciones() {
43
44
  ascii: { type: 'boolean' },
44
45
  mouse: { type: 'boolean' },
45
46
  'no-mouse': { type: 'boolean' },
47
+ 'no-animation': { type: 'boolean' },
48
+ animation: { type: 'boolean' },
46
49
  version: { type: 'boolean', short: 'v' },
47
50
  help: { type: 'boolean', short: 'h' },
48
51
  browser: { type: 'boolean' },
@@ -90,7 +93,7 @@ export function modosEnColor() {
90
93
  return `${nombres.slice(0, 3).join(', ')} or ${nombres[3]}`;
91
94
  }
92
95
  function ayudaDeComandos() {
93
- 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')}` });
94
97
  }
95
98
  async function cuentaOError(cred, pantalla) {
96
99
  try {
@@ -262,10 +265,40 @@ async function principal() {
262
265
  const quien = quienEs(cuenta);
263
266
  let avisos = ajustes.avisos !== false;
264
267
  const conRaton = !v['no-mouse'] && v.mouse !== false && (Boolean(v.mouse) || ajustes.raton !== false);
268
+ pantalla.animar = !v['no-animation'] && v.animation !== false && (Boolean(v.animation) || ajustes.animacion !== false);
265
269
  let esperandoEnBandeja = null;
266
270
  let control = new AbortController();
267
271
  let ocupado = false;
268
272
  let superficie = null;
273
+ let bienvenida;
274
+ let consejosDeEntrada = [];
275
+ const confirmarNuevaEnMarcha = async () => {
276
+ if (!superficie)
277
+ return;
278
+ const i = await superficie.elegir(t('nueva.parar'), [{ etiqueta: t('nueva.si'), peligro: true }, { etiqueta: t('nueva.no') }], { detalle: t('nueva.parar_detalle'), elegida: 1 });
279
+ if (i !== 0)
280
+ return;
281
+ superficie.nuevaTrasParar();
282
+ if (ocupado)
283
+ control.abort();
284
+ };
285
+ const empezarDeNuevo = () => {
286
+ sesion.conversacion = [];
287
+ sesion.diario = [];
288
+ sesion.id = randomUUID();
289
+ sesion.ultimaTarea = undefined;
290
+ sesion.ctx.hechos = { ficheros: [], comandos: [] };
291
+ sesion.ctx.permitidos.clear();
292
+ sesion.ctx.deshacer.length = 0;
293
+ if (sesion.ctx.lecturas)
294
+ sesion.ctx.lecturas.length = 0;
295
+ registro.apuntar('nueva', {});
296
+ pantalla.reiniciar();
297
+ if (superficie)
298
+ superficie.reiniciar(bienvenida, consejosDeEntrada);
299
+ else
300
+ pantalla.escribir(t('repl.nueva'));
301
+ };
269
302
  let ctrlC = 0;
270
303
  const totalDeLaSesion = () => sesion.total.tareas ? c.gris(t('estado.total', { acciones: acciones(sesion.total.acciones), tiempo: duracion(sesion.total.ms) })) : '';
271
304
  const despedida = () => [totalDeLaSesion(), c.gris(t('repl.adios'))].filter(Boolean).join('\n ');
@@ -401,6 +434,25 @@ async function principal() {
401
434
  extra: sesion.ensenar ? 'Teach' : undefined,
402
435
  esperando: esperandoEnBandeja ?? 0,
403
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
+ };
404
456
  if (interactiva) {
405
457
  superficie = new Superficie({
406
458
  entrada: process.stdin,
@@ -419,6 +471,18 @@ async function principal() {
419
471
  if (avisos && superficie?.enfocada === false)
420
472
  avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
421
473
  },
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
+ },
422
486
  });
423
487
  const s = superficie;
424
488
  process.on('exit', () => s.restaurarDeUrgencia());
@@ -437,7 +501,8 @@ async function principal() {
437
501
  const elegidos = elegirConsejos(leerAjustes().consejos);
438
502
  if (elegidos.ids.length)
439
503
  guardarAjustes({ consejos: elegidos.estado });
440
- superficie.iniciar(bloqueDeBienvenida({
504
+ consejosDeEntrada = conCarriles || quiereBypass ? [] : elegidos.ids;
505
+ bienvenida = bloqueDeBienvenida({
441
506
  version: VERSION,
442
507
  quien: quien.includes('@') ? quien : (quien.split(/\s+/)[0] ?? ''),
443
508
  carpeta: rutaCorta(raiz, 60),
@@ -447,7 +512,8 @@ async function principal() {
447
512
  return nueva ? t('update.disponible', { version: nueva }) : undefined;
448
513
  })(),
449
514
  peligro: () => sesion.ctx.modo === 'bypass',
450
- }), conCarriles || quiereBypass ? [] : elegidos.ids);
515
+ });
516
+ superficie.iniciar(bienvenida, consejosDeEntrada);
451
517
  if (quiereBypass)
452
518
  await confirmarBypass();
453
519
  const mirarBandeja = async () => {
@@ -469,6 +535,7 @@ async function principal() {
469
535
  const relojBandeja = setInterval(() => void mirarBandeja(), 90_000);
470
536
  relojBandeja.unref();
471
537
  void mirarBandeja();
538
+ void cargarConexiones();
472
539
  }
473
540
  const leerLinea = async (ejemplo = null) => {
474
541
  if (superficie)
@@ -605,6 +672,147 @@ async function principal() {
605
672
  elegida = Math.min(elegida, lista.length - 1);
606
673
  }
607
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
+ };
608
816
  const bash = async (comando) => {
609
817
  if (!comando)
610
818
  return;
@@ -811,6 +1019,23 @@ async function principal() {
811
1019
  await bandeja();
812
1020
  superficie?.pintar();
813
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
+ }
814
1039
  case 'mouse': {
815
1040
  const si = ALIAS_SI_NO[arg] ?? (superficie?.conRaton ? 'no' : 'si');
816
1041
  guardarAjustes({ raton: si === 'si' });
@@ -819,9 +1044,15 @@ async function principal() {
819
1044
  break;
820
1045
  }
821
1046
  case 'new':
822
- sesion.conversacion = [];
823
- pantalla.escribir(t('repl.nueva'));
1047
+ empezarDeNuevo();
824
1048
  break;
1049
+ case 'animation': {
1050
+ const si = ALIAS_SI_NO[arg] ?? (pantalla.animar ? 'no' : 'si');
1051
+ pantalla.animar = si === 'si';
1052
+ guardarAjustes({ animacion: pantalla.animar });
1053
+ pantalla.escribir(pantalla.animar ? t('animacion.on') : t('animacion.off'));
1054
+ break;
1055
+ }
825
1056
  case 'undo': {
826
1057
  const r2 = deshacerUltimo(sesion.ctx);
827
1058
  pantalla.escribir(r2 ? t('repl.deshecho', { ruta: r2 }) : t('repl.nada_que_deshacer'));