@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 +15 -0
- package/README.md +26 -6
- package/dist/actividad.js +131 -0
- package/dist/agente.js +35 -8
- package/dist/api.js +18 -0
- package/dist/cli.js +238 -7
- package/dist/comandos.js +71 -0
- package/dist/config.js +4 -0
- package/dist/consejos.js +2 -1
- package/dist/herramientas.js +18 -4
- package/dist/menciones.js +85 -0
- package/dist/textos.js +72 -10
- package/dist/tui.js +197 -34
- package/dist/ui.js +143 -35
- package/package.json +1 -1
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
| `/
|
|
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.
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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'));
|