@getparallel/parx 0.4.1 → 0.4.3

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,31 @@ 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.4.3 (2026-10-11)
10
+
11
+ - 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.
12
+
13
+ ## 0.4.2 (2026-10-11)
14
+
15
+ - Bash mode: type `!` first and the input turns amber with a `bash` label; Enter runs the command in the project folder, its output streams into the conversation with the exit code, and the agent sees it next turn. Read only refuses, «Careful» commands ask first, credentials stay out of the conversation, Esc or Ctrl+C stops it.
16
+ - Everything is clickable: permission questions, Accept/Decline, the `/` menu and its values, `/inbox` and the status row are buttons («[ 1 Yes ] [ 2 Always ] [ 3 No ]») with hover; the keyboard works as before. `/mouse on|off` and `--no-mouse`; Shift+drag selects text. The terminal always gets the mouse back (exit, Ctrl+Z, kill, crash).
17
+ - The wheel and PgUp/PgDn scroll the conversation; «↓ Jump to bottom · N new» (or End) takes you back down. New output waits below while you read.
18
+ - `/inbox` and «● 2 waiting»: answer what your Parallel missions are waiting for (approvals, proposed changes, your steps) from parx. Needs the matching getparallel.net release.
19
+ - More air: a blank row between the conversation and the input and between the input and the status row, a blank row between groups (your message, actions, warnings, answers), the question as its own block, never two blank rows in a row. «Careful: leaves the project folder.» instead of the long sentence.
20
+ - Ctrl+Z suspends parx like any terminal program; `fg` brings it back.
21
+ - A question no longer hides the last lines above it: the warning it explains («never to the log.») stays on screen, and the answer is written whole at any width.
22
+ - Credentials: when you ask for something that needs one, parx asks you (it no longer refuses on its own), and a «No» only counts for that task.
23
+ - `/teach` explains every step and ends with a short lesson, also with Swift; `/teach <task>` turns it on and does the task.
24
+ - Typing a whole command and Enter runs it (`/teach`, `/notify`, `/tips`, `/folder`) instead of waiting for a value.
25
+ - `/help` and `/tips` wrap in two clean columns at any width, from 40 columns up.
26
+ - Clearer notes: «3 lines», «2 items», «no matches», «… 8 more lines above»; test output shows pass and fail instead of the runner's filler.
27
+ - `/mission` gives the link to the mission itself once it launches.
28
+ - `/report` copies plain text (no backticks or asterisks) that reads well in an email or WhatsApp.
29
+ - «Running tests» and «Building» only for real test and build commands, not for files like `sum.test.js`.
30
+ - With `--lanes` in a narrow window, each lane keeps its task name instead of a lone «…».
31
+ - Everything you type while parx works (or several lines at once) waits in line, in order; before, only the last one was kept.
32
+ - A permission question that pops up while you are typing your next task is not answered by your Enter: what you typed stays in the box. Esc still answers «No» at once.
33
+
9
34
  ## 0.4.1 (2026-10-10)
10
35
 
11
36
  - El nombre visible es siempre «parx», nunca «Par» (consejos, avisos, README)
package/README.md CHANGED
@@ -78,8 +78,8 @@ when it asks first:
78
78
  | **Bypass** | no questions anywhere | no questions |
79
79
 
80
80
  - **Bypass** is never the default and never saved: turn it on yourself with `--bypass`, or with
81
- `/mode bypass`, a clear warning and typing `bypass` to confirm. The status line shows `▲ Bypass` in
82
- amber while it is on. Shift+Tab (Ask → Auto → Read only) leaves it.
81
+ `/mode bypass` and a clear warning to accept (Decline comes selected; you pick Accept). The status
82
+ line shows `▲ Bypass` in red while it is on. Shift+Tab (Ask → Auto → Read only) leaves it.
83
83
  - **Credentials always ask**, in every mode including Bypass: `.env` files, SSH keys, `~/.aws`,
84
84
  `.npmrc`, keychains, tokens. Each time it needs an explicit «Yes, this once» (No comes selected).
85
85
  Their paths, commands and contents never go to the log, and commands run with secret-looking
@@ -107,11 +107,29 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
107
107
  | `/folder <path>` | let parx work in another folder |
108
108
  | `/lanes` | 2 or 3 tasks at once, each in its own lane, all moving together |
109
109
  | `/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 |
110
+ | `/inbox` | what your missions are waiting for (approvals, proposed changes, your steps), as buttons; what you pick takes effect like on the web |
110
111
  | `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
111
112
  | `/teach` | class mode: explains each step as it codes, like a teacher |
112
113
  | `/undo`, `/new`, `/balance`, `/log`, `/help`, `/exit` | the usual |
113
114
  | `/notify` on, off | desktop notifications |
114
115
  | `/tips` on, off | the welcome tips |
116
+ | `/mouse` on, off | clicks and wheel inside parx (saved; `--no-mouse` for one session) |
117
+
118
+ **Bash mode:** start the input with `!` and it turns amber with a small `bash` label; Enter runs the
119
+ command in your shell, in the project folder, and its output streams into the conversation with the
120
+ exit code. The agent sees the command and its output in the next task. Backspace on a lone `!`
121
+ leaves the mode; Esc or Ctrl+C stops a running command. Read only runs nothing; a command with
122
+ effects outside the project («Careful: leaves the project folder.») asks first (not in Bypass); the
123
+ output of a command that touches credentials stays on your screen and out of the conversation.
124
+
125
+ **Mouse:** click any option, button or menu item (permission questions, Accept/Decline, `/` menu,
126
+ `/inbox`); hovering highlights but never picks. The wheel or PgUp/PgDn scroll the conversation; when
127
+ you are up, a «↓ Jump to bottom» button (with «N new» if output arrived) takes you back, and so does
128
+ End. To select text, drag with Shift (Option in iTerm2, Fn in macOS Terminal), or `/mouse off`. The
129
+ terminal is always given back as it was: on exit, on Ctrl+Z, if parx is killed or crashes.
130
+
131
+ **Waiting for you:** the status row shows «● 2 waiting» when your Parallel missions need an answer;
132
+ click it or type `/inbox`. parx never approves anything on its own: always your click or your key.
115
133
 
116
134
  **Images:** drag a screenshot into the window (or paste its path) and it becomes a chip
117
135
  (`[Image · error.png]`); parx sends it with the task to understand the error or the design. PNG,
package/dist/agente.js CHANGED
@@ -29,15 +29,17 @@ const PERMISOS = {
29
29
  read: 'Read only: you can read and search, but you cannot write files or run commands. Explain what you would change instead.',
30
30
  bypass: 'Bypass: the user turned off the questions for this session; you can work anywhere on this computer. Be careful and do only what the task needs.',
31
31
  };
32
+ export const RECORDATORIO_DE_CLASE = '(Teach mode is on: before each step, explain in 1-3 plain sentences what you will do and why; at the end, add a short lesson for a beginner.)';
32
33
  export function instrucciones(raiz, modo, o = {}) {
33
34
  const carpetas = o.carpetas?.length ? `\n- Allowed folders besides the project: ${o.carpetas.join(', ')}` : '';
34
35
  const clase = o.ensenar
35
36
  ? `
36
37
 
37
38
  # Teach mode (the user turned it on)
38
- - Work like a patient teacher. Before each step, say in 1-3 plain sentences what you are about to do and why.
39
+ - Work like a patient teacher. Before EVERY tool call, write 1-3 plain sentences (as text, in the same reply) saying what you are about to do and why. Never call a tool without that explanation first.
39
40
  - After each change, explain the idea behind it (the concept, not just the code), as if to a beginner. Define any technical word the first time you use it.
40
- - Keep each explanation short and concrete; use the project's own code as the example.`
41
+ - Keep each explanation short and concrete; use the project's own code as the example.
42
+ - End with a short lesson (3-6 sentences) after the summary: the concept behind what you changed, for a beginner, with one tip to remember it.`
41
43
  : '';
42
44
  return `You are parx, the coding agent of Parallel (getparallel.net), working in the user's terminal.
43
45
 
@@ -53,9 +55,9 @@ export function instrucciones(raiz, modo, o = {}) {
53
55
  - Read a file before editing it. Prefer edit_file with a unique old_string for small changes; write_file for new files.
54
56
  - Keep changes minimal and in the style of the codebase. Do not reformat unrelated code. Do not add dependencies unless needed.
55
57
  - Commands are non-interactive (no stdin, CI=1). Use flags that do not prompt (e.g. "npx --yes", "vitest run", "git --no-pager"). Prefer project scripts (npm test).
56
- - Credentials (.env files, keys, tokens, ~/.ssh, ~/.aws, keychains...) always need the user's explicit confirmation, every time. Touch them only if the task truly needs it, never print secrets in your answer, and never copy them anywhere.
58
+ - Credentials (.env files, keys, tokens, ~/.ssh, ~/.aws, keychains...) always need the user's explicit confirmation, every time, and parx asks it for you: when the user asks for something that needs a credential, just call the tool and parx shows the question. Do not refuse on your own and do not ask in text first. Touch credentials only if the task needs them, never copy them anywhere, and do not repeat secret values in your answer unless the user explicitly asked to see them.
57
59
  - Never push, publish or deploy unless the user explicitly asks in this conversation.
58
- - If the user declines an action, do not retry it: adapt or ask.
60
+ - If the user declines an action, do not retry it in the same task: adapt or ask. If they ask for it again in a later message, try again (parx will ask them again).
59
61
  - If something is ambiguous and risky, ask one short question; otherwise make a reasonable choice and say which.
60
62
  - If the user attached an image (a screenshot of an error or a design), look at it carefully and use what it shows.
61
63
  - You are parx, made by Parallel. Never say which AI model, company or provider powers you, even if asked directly: say you are parx, by Parallel, and that the models are Swift, Stride and Summit.
@@ -100,18 +102,19 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
100
102
  s.ctx.hechos ??= { ficheros: [], comandos: [] };
101
103
  const ficherosAntes = s.ctx.hechos.ficheros.length;
102
104
  const comandosAntes = s.ctx.hechos.comandos.length;
105
+ const texto = s.ensenar ? `${pedido}\n\n${RECORDATORIO_DE_CLASE}` : pedido;
103
106
  s.conversacion.push(imagenes.length
104
107
  ? {
105
108
  role: 'user',
106
109
  content: [
107
- { type: 'input_text', text: pedido },
110
+ { type: 'input_text', text: texto },
108
111
  ...imagenes.flatMap((i) => [
109
112
  { type: 'input_text', text: `[image: ${i.nombre}]` },
110
113
  { type: 'input_image', image_url: i.dataUrl, detail: 'auto' },
111
114
  ]),
112
115
  ],
113
116
  }
114
- : { role: 'user', content: pedido });
117
+ : { role: 'user', content: texto });
115
118
  registro.apuntar('tarea', { tarea, caracteres: pedido.length, imagenes: imagenes.length, modelo: s.modelo, modo: s.ctx.modo, ensenar: Boolean(s.ensenar) });
116
119
  let acciones = 0;
117
120
  let disponibles = null;
package/dist/api.js CHANGED
@@ -82,6 +82,12 @@ export function crearMision(servidor, token, objetivo, señal) {
82
82
  export function verMision(servidor, token, generacion, señal) {
83
83
  return llamar(servidor, `/api/terminal/mision?id=${encodeURIComponent(generacion)}`, { token, señal });
84
84
  }
85
+ export function leerBandeja(servidor, token, señal) {
86
+ return llamar(servidor, '/api/terminal/bandeja', { token, señal, plazoMs: 20_000 });
87
+ }
88
+ export function responderBandeja(servidor, token, cuerpo, señal) {
89
+ return llamar(servidor, '/api/terminal/bandeja', { token, cuerpo, señal, plazoMs: 60_000 });
90
+ }
85
91
  const CORREO_INTERNO = /@cuentas\.getparallel\.net$/i;
86
92
  export function correoVisible(correo) {
87
93
  const c = (correo ?? '').trim();
package/dist/bash.js ADDED
@@ -0,0 +1,89 @@
1
+ import { nivelDeComando } from "./seguridad.js";
2
+ import { ejecutarComando, recortar } from "./shell.js";
3
+ import { sinColor } from "./texto.js";
4
+ import { t } from "./textos.js";
5
+ import { bloqueDeFinDeBash, bloqueDeSalidaDeBash, duracion } from "./tui.js";
6
+ export const MAX_LINEAS_A_LA_VISTA = 1000;
7
+ export const PLAZO_BASH_MS = 600_000;
8
+ export function lineasLimpias(texto) {
9
+ return sinColor(texto)
10
+ .split('\n')
11
+ .map((l) => {
12
+ const partes = l.replace(/\r+$/, '').split('\r');
13
+ return partes[partes.length - 1] ?? '';
14
+ });
15
+ }
16
+ export function textoParaElAgente(comando, r) {
17
+ if (!r.compartir)
18
+ return '(I ran a command in my own shell that touches credentials. Its output was kept out of this conversation on purpose.)';
19
+ const cabecera = r.plazoAgotado ? 'Timed out (killed).' : r.interrumpido ? 'Stopped by me before it finished.' : `Exit code: ${r.codigo}`;
20
+ return `(I ran this myself in my shell, in the project folder:\n$ ${comando}\n${cabecera}\n${recortar(r.salida.replace(/\r/g, ''), 12_000).trim() || '(no output)'})`;
21
+ }
22
+ export async function ejecutarBash(comando, ctx, señal, alEmpezar) {
23
+ const p = ctx.pantalla;
24
+ if (ctx.modo === 'read') {
25
+ p.aviso(t('bash.solo_lectura'));
26
+ ctx.registro.apuntar('bloqueado', { herramienta: 'bash', motivo: 'solo_lectura' });
27
+ return null;
28
+ }
29
+ const { nivel, motivo } = nivelDeComando(comando);
30
+ const credencial = nivel === 'credencial';
31
+ if (nivel === 'riesgo' && ctx.modo !== 'bypass') {
32
+ const r = await p.preguntar(t('pregunta.ejecutar'), { tipo: 'dos', aviso: t('aviso.riesgo', { motivo: motivo ?? '' }) });
33
+ if (r === 'no') {
34
+ ctx.registro.apuntar('rechazado', { herramienta: 'bash', comando: comando.slice(0, 500) });
35
+ return null;
36
+ }
37
+ }
38
+ alEmpezar?.();
39
+ p.marcarGrupo('accion');
40
+ let pendiente = '';
41
+ let vistas = 0;
42
+ let ocultas = 0;
43
+ const soltar = (todo) => {
44
+ const partes = pendiente.split('\n');
45
+ pendiente = todo ? '' : (partes.pop() ?? '');
46
+ const lineas = lineasLimpias(partes.join('\n'));
47
+ if (todo && lineas.length && lineas[lineas.length - 1] === '')
48
+ lineas.pop();
49
+ if (!partes.length || !lineas.length)
50
+ return;
51
+ const caben = Math.max(0, MAX_LINEAS_A_LA_VISTA - vistas);
52
+ const ahora = lineas.slice(0, caben);
53
+ ocultas += lineas.length - ahora.length;
54
+ vistas += ahora.length;
55
+ if (ahora.length)
56
+ p.bloque(bloqueDeSalidaDeBash(ahora), 'accion');
57
+ };
58
+ const reloj = setInterval(() => soltar(false), 80);
59
+ let res;
60
+ try {
61
+ res = await ejecutarComando(comando, {
62
+ cwd: ctx.raiz,
63
+ plazoMs: PLAZO_BASH_MS,
64
+ jaula: Boolean(ctx.jaula),
65
+ carpetas: ctx.carpetas ?? [],
66
+ señal,
67
+ alSalir: (trozo) => {
68
+ pendiente += trozo;
69
+ },
70
+ });
71
+ }
72
+ finally {
73
+ clearInterval(reloj);
74
+ }
75
+ soltar(true);
76
+ if (ocultas)
77
+ p.bloque(bloqueDeSalidaDeBash([t('bash.mas', { n: ocultas.toLocaleString('en-US') })]), 'accion');
78
+ const bien = res.codigo === 0 && !res.plazoAgotado && !res.interrumpido;
79
+ const tiempo = duracion(res.ms);
80
+ const fin = res.plazoAgotado ? `timeout · ${tiempo}` : res.interrumpido ? `${t('estado.interrumpido').replace(/\.$/, '')} · ${tiempo}` : `exit ${res.codigo ?? '?'} · ${tiempo}`;
81
+ p.bloque(bloqueDeFinDeBash(bien, vistas ? fin : `${t('bash.sin_salida')} · ${fin}`), 'accion');
82
+ if (credencial)
83
+ p.escribir(t('bash.no_compartido'), 'accion');
84
+ ctx.hechos ??= { ficheros: [], comandos: [] };
85
+ if (!credencial)
86
+ ctx.hechos.comandos.push({ comando: comando.slice(0, 200), codigo: res.codigo });
87
+ ctx.registro.apuntar('bash', { comando: credencial ? '[credential]' : comando.slice(0, 500), codigo: res.codigo, ms: res.ms, jaula: res.enJaula });
88
+ return { codigo: res.codigo, salida: res.salida, ms: res.ms, interrumpido: res.interrumpido, plazoAgotado: res.plazoAgotado, compartir: !credencial };
89
+ }
package/dist/cli.js CHANGED
@@ -6,24 +6,25 @@ import { basename, resolve } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { ALIAS_COMANDO, ALIAS_SI_NO, COMANDOS, t, USO } from "./textos.js";
9
- import { c, fijarCapacidades, sep, signo } from "./marca.js";
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, leerCuenta, quienEs, verMision } from "./api.js";
12
+ import { correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerCuenta, quienEs, responderBandeja, verMision } from "./api.js";
13
13
  import { comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
14
14
  import { hacerTarea, mensajeDeError, nuevaSesion } from "./agente.js";
15
15
  import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
16
16
  import { Registro } from "./registro.js";
17
17
  import { Pantalla } from "./ui.js";
18
- import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, duracion, Superficie } from "./tui.js";
18
+ import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, bloqueDeTabla, duracion, Superficie } from "./tui.js";
19
19
  import { CONSEJOS, cambiarConsejos, consejo, elegirConsejos, ordenDeConsejos } from "./consejos.js";
20
20
  import { anchoDe, recortarMedio } from "./texto.js";
21
21
  import { cargarImagen, ErrorDeImagen, extraerImagenes } from "./imagenes.js";
22
22
  import { avisarEnElEscritorio, debeAvisar } from "./notificar.js";
23
- import { copiarAlPortapapeles, escribirInforme } from "./informe.js";
23
+ import { copiarAlPortapapeles, escribirInforme, textoParaPegar } from "./informe.js";
24
24
  import { objetivoDeMision } from "./mision.js";
25
25
  import { ejecutarCarriles, MAX_CARRILES } from "./carriles.js";
26
26
  import { spawnSync } from 'node:child_process';
27
+ import { ejecutarBash, textoParaElAgente } from "./bash.js";
27
28
  import { comprobarDeFondo, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
28
29
  function opciones() {
29
30
  return parseArgs({
@@ -40,6 +41,8 @@ function opciones() {
40
41
  lanes: { type: 'boolean' },
41
42
  dir: { type: 'string', short: 'C' },
42
43
  ascii: { type: 'boolean' },
44
+ mouse: { type: 'boolean' },
45
+ 'no-mouse': { type: 'boolean' },
43
46
  version: { type: 'boolean', short: 'v' },
44
47
  help: { type: 'boolean', short: 'h' },
45
48
  browser: { type: 'boolean' },
@@ -82,10 +85,12 @@ export function modoDe(texto) {
82
85
  return tabla[v] ?? null;
83
86
  }
84
87
  export const SIGUIENTE_MODO = { ask: 'auto', auto: 'read', read: 'ask', bypass: 'ask' };
88
+ export function modosEnColor() {
89
+ const nombres = ['read', 'ask', 'auto', 'bypass'].map((m) => colorDeModo(m)(nombreDeModo(m)));
90
+ return `${nombres.slice(0, 3).join(', ')} or ${nombres[3]}`;
91
+ }
85
92
  function ayudaDeComandos() {
86
- const ancho = Math.max(...COMANDOS.map(([k]) => anchoDe(k))) + 4;
87
- const filas = COMANDOS.map(([k, d]) => ` ${c.marca(`/${k}`.padEnd(ancho))}${d}`);
88
- return [...filas, '', c.gris(t('tui.atajos'))].join('\n');
93
+ return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, k === 'mode' ? modosEnColor() : d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_raton')}` });
89
94
  }
90
95
  async function cuentaOError(cred, pantalla) {
91
96
  try {
@@ -256,6 +261,8 @@ async function principal() {
256
261
  const saldoCorto = () => (disponibles == null ? '' : disponibles.toLocaleString('en-US'));
257
262
  const quien = quienEs(cuenta);
258
263
  let avisos = ajustes.avisos !== false;
264
+ const conRaton = !v['no-mouse'] && v.mouse !== false && (Boolean(v.mouse) || ajustes.raton !== false);
265
+ let esperandoEnBandeja = null;
259
266
  let control = new AbortController();
260
267
  let ocupado = false;
261
268
  let superficie = null;
@@ -390,7 +397,9 @@ async function principal() {
390
397
  saldo: saldo(),
391
398
  saldoCorto: saldoCorto(),
392
399
  peligro: sesion.ctx.modo === 'bypass',
400
+ clave: sesion.ctx.modo,
393
401
  extra: sesion.ensenar ? 'Teach' : undefined,
402
+ esperando: esperandoEnBandeja ?? 0,
394
403
  });
395
404
  if (interactiva) {
396
405
  superficie = new Superficie({
@@ -404,6 +413,8 @@ async function principal() {
404
413
  guardarAjustes({ modo: sesion.ctx.modo });
405
414
  },
406
415
  alSalir: adios,
416
+ raton: conRaton,
417
+ alBandeja: () => superficie?.escribirYMandar('/inbox'),
407
418
  alPreguntar: (texto) => {
408
419
  if (avisos && superficie?.enfocada === false)
409
420
  avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
@@ -411,6 +422,17 @@ async function principal() {
411
422
  });
412
423
  const s = superficie;
413
424
  process.on('exit', () => s.restaurarDeUrgencia());
425
+ for (const [señal, codigo] of [['SIGTERM', 143], ['SIGHUP', 129]]) {
426
+ process.on(señal, () => {
427
+ try {
428
+ s.terminar();
429
+ }
430
+ catch {
431
+ s.restaurarDeUrgencia();
432
+ }
433
+ process.exit(codigo);
434
+ });
435
+ }
414
436
  pantalla.fijarSuperficie(superficie);
415
437
  const elegidos = elegirConsejos(leerAjustes().consejos);
416
438
  if (elegidos.ids.length)
@@ -428,6 +450,25 @@ async function principal() {
428
450
  }), conCarriles || quiereBypass ? [] : elegidos.ids);
429
451
  if (quiereBypass)
430
452
  await confirmarBypass();
453
+ const mirarBandeja = async () => {
454
+ if (ocupado)
455
+ return;
456
+ try {
457
+ const r = await leerBandeja(cred.servidor, cred.token);
458
+ const n = r.elementos.length;
459
+ if (n !== esperandoEnBandeja) {
460
+ esperandoEnBandeja = n;
461
+ superficie?.pintar();
462
+ }
463
+ }
464
+ catch (e) {
465
+ if (e instanceof ErrorDeParallel && (e.codigo === 'sin_acceso' || e.codigo === 'sesion'))
466
+ clearInterval(relojBandeja);
467
+ }
468
+ };
469
+ const relojBandeja = setInterval(() => void mirarBandeja(), 90_000);
470
+ relojBandeja.unref();
471
+ void mirarBandeja();
431
472
  }
432
473
  const leerLinea = async (ejemplo = null) => {
433
474
  if (superficie)
@@ -457,6 +498,131 @@ async function principal() {
457
498
  }
458
499
  return tareas;
459
500
  }
501
+ const pedirTexto = async (e, opcion) => {
502
+ const obligatorio = opcion === 'cambios' && e.texto_libre;
503
+ const opcional = e.tipo === 'paso' && opcion === 'hecho' && e.texto_libre;
504
+ if (!obligatorio && !opcional)
505
+ return undefined;
506
+ const l = ((await leerLinea(t(obligatorio ? 'bandeja.escribe' : 'bandeja.nota'))) ?? '').trim();
507
+ if (!l && obligatorio)
508
+ return null;
509
+ return l || undefined;
510
+ };
511
+ const bandeja = async () => {
512
+ const leer = async () => {
513
+ ocupado = true;
514
+ control = new AbortController();
515
+ superficie?.trabajar(() => control.abort());
516
+ pantalla.espera(t('bandeja.mirando'), 'ruta', true);
517
+ try {
518
+ const r = await leerBandeja(cred.servidor, cred.token, control.signal);
519
+ esperandoEnBandeja = r.elementos.length;
520
+ return r.elementos;
521
+ }
522
+ catch (e) {
523
+ if (!(e instanceof ErrorDeParallel))
524
+ throw e;
525
+ if (e.codigo === 'sin_acceso')
526
+ pantalla.error(t('bandeja.no_disponible'));
527
+ else if (e.codigo !== 'interrumpido')
528
+ pantalla.error(mensajeDeError(e));
529
+ return null;
530
+ }
531
+ finally {
532
+ pantalla.pararEspera();
533
+ ocupado = false;
534
+ superficie?.descansar();
535
+ }
536
+ };
537
+ let lista = await leer();
538
+ if (!lista)
539
+ return;
540
+ if (!lista.length) {
541
+ pantalla.escribir(t('bandeja.nada'));
542
+ return;
543
+ }
544
+ if (!superficie) {
545
+ for (const e of lista)
546
+ pantalla.escribir(`${e.titulo}${c.gris(`${sep()}${e.mision}`)} ${c.gris(e.url)}`);
547
+ return;
548
+ }
549
+ let elegida = 0;
550
+ for (;;) {
551
+ const i = await superficie.elegir(t('bandeja.titulo'), lista.map((e) => ({ etiqueta: e.titulo, nota: e.mision })), { vertical: true, elegida });
552
+ if (i === null)
553
+ break;
554
+ elegida = i;
555
+ const e = lista[i];
556
+ if (!e.opciones.length) {
557
+ pantalla.escribir(t('bandeja.abrir', { url: c.marca(e.url) }));
558
+ continue;
559
+ }
560
+ const recomendada = Math.max(0, e.opciones.findIndex((o) => o.recomendada));
561
+ const j = await superficie.elegir(e.titulo, [...e.opciones.map((o) => ({ etiqueta: o.etiqueta, recomendada: o.recomendada, peligro: o.peligro })), { etiqueta: t('bandeja.volver') }], { detalle: [e.mision, e.detalle].filter(Boolean).join(sep()), elegida: recomendada });
562
+ if (j === null || j >= e.opciones.length)
563
+ continue;
564
+ const opcion = e.opciones[j];
565
+ const texto = await pedirTexto(e, opcion.id);
566
+ if (texto === null) {
567
+ pantalla.escribir(c.gris(t('bandeja.cancelado')));
568
+ continue;
569
+ }
570
+ ocupado = true;
571
+ control = new AbortController();
572
+ superficie.trabajar(() => control.abort());
573
+ pantalla.espera(t('bandeja.mirando'), 'ruta', true);
574
+ try {
575
+ const r = await responderBandeja(cred.servidor, cred.token, { id: e.id, opcion: opcion.id, ...(texto ? { texto } : {}) }, control.signal);
576
+ registro.apuntar('bandeja', { tipo: e.tipo, opcion: opcion.id.replace(/:.*/, '') });
577
+ const hecho = { approved: 'bandeja.hecho_aprobado', rejected: 'bandeja.hecho_rechazado', revising: 'bandeja.hecho_cambios', done: 'bandeja.hecho_paso', skipped: 'bandeja.hecho_saltado' };
578
+ pantalla.escribir(`${c.ok(SIMBOLOS.hecho)} ${t(hecho[r.estado] ?? 'bandeja.hecho_aprobado')}${c.gris(`${sep()}${e.mision}`)}`);
579
+ }
580
+ catch (err) {
581
+ if (!(err instanceof ErrorDeParallel))
582
+ throw err;
583
+ if (err.codigo === 'servidor' && err.detalle.startsWith('409'))
584
+ pantalla.aviso(t('bandeja.ya'));
585
+ else if (err.codigo === 'sin_acceso')
586
+ pantalla.aviso(t('bandeja.no_existe'));
587
+ else if (err.codigo === 'servidor' && err.detalle.startsWith('429'))
588
+ pantalla.aviso(t('bandeja.despacio'));
589
+ else if (err.codigo !== 'interrumpido')
590
+ pantalla.error(mensajeDeError(err));
591
+ }
592
+ finally {
593
+ pantalla.pararEspera();
594
+ ocupado = false;
595
+ superficie.descansar();
596
+ }
597
+ lista = await leer();
598
+ superficie.pintar();
599
+ if (!lista)
600
+ return;
601
+ if (!lista.length) {
602
+ pantalla.escribir(t('bandeja.todo'));
603
+ return;
604
+ }
605
+ elegida = Math.min(elegida, lista.length - 1);
606
+ }
607
+ };
608
+ const bash = async (comando) => {
609
+ if (!comando)
610
+ return;
611
+ control = new AbortController();
612
+ try {
613
+ const r = await ejecutarBash(comando, sesion.ctx, control.signal, () => {
614
+ ocupado = true;
615
+ superficie?.trabajar(() => control.abort());
616
+ superficie?.textoDeTrabajo(t('bash.corriendo', { comando: comando.length > 28 ? `${comando.slice(0, 27)}…` : comando }), 'tren', true);
617
+ });
618
+ if (r)
619
+ sesion.conversacion.push({ role: 'user', content: textoParaElAgente(comando, r) });
620
+ }
621
+ finally {
622
+ ocupado = false;
623
+ superficie?.descansar();
624
+ }
625
+ };
460
626
  for (;;) {
461
627
  const r = await leerLinea();
462
628
  if (r === null)
@@ -465,6 +631,10 @@ async function principal() {
465
631
  ctrlC = 0;
466
632
  if (!linea)
467
633
  continue;
634
+ if (linea.startsWith('!')) {
635
+ await bash(linea.slice(1).trim());
636
+ continue;
637
+ }
468
638
  if (linea.startsWith('/')) {
469
639
  const [nombre = '', ...args] = linea.slice(1).split(/\s+/);
470
640
  const cmd = ALIAS_COMANDO[nombre.toLowerCase()];
@@ -479,7 +649,7 @@ async function principal() {
479
649
  rl?.close();
480
650
  return 0;
481
651
  case 'help':
482
- pantalla.escribir(ayudaDeComandos());
652
+ pantalla.bloque(ayudaDeComandos());
483
653
  break;
484
654
  case 'mode': {
485
655
  const m = modoDe(arg);
@@ -497,7 +667,7 @@ async function principal() {
497
667
  guardarAjustes({ modo: m });
498
668
  registro.apuntar('modo', { modo: m });
499
669
  }
500
- pantalla.escribir(t('modo.ahora', { modo: nombreDeModo(sesion.ctx.modo), desc: t(`modo.${sesion.ctx.modo}.desc`) }));
670
+ pantalla.escribir(t('modo.ahora', { modo: colorDeModo(sesion.ctx.modo)(c.negrita(nombreDeModo(sesion.ctx.modo))), desc: t(`modo.${sesion.ctx.modo}.desc`) }));
501
671
  superficie?.pintar();
502
672
  break;
503
673
  }
@@ -534,10 +704,18 @@ async function principal() {
534
704
  break;
535
705
  }
536
706
  case 'teach': {
537
- const si = ALIAS_SI_NO[arg] ?? (sesion.ensenar ? 'no' : 'si');
707
+ const tareaConClase = !ALIAS_SI_NO[arg] && resto ? resto : null;
708
+ const si = tareaConClase ? 'si' : (ALIAS_SI_NO[arg] ?? (sesion.ensenar ? 'no' : 'si'));
709
+ const cambia = sesion.ensenar !== (si === 'si');
538
710
  sesion.ensenar = si === 'si';
539
- pantalla.escribir(sesion.ensenar ? t('repl.ensenar_on') : t('repl.ensenar_off'));
711
+ if (cambia || !tareaConClase)
712
+ pantalla.escribir(sesion.ensenar ? t('repl.ensenar_on') : t('repl.ensenar_off'));
540
713
  superficie?.pintar();
714
+ if (tareaConClase && !(await hacer(tareaConClase))) {
715
+ superficie?.terminar(despedida());
716
+ rl?.close();
717
+ return 1;
718
+ }
541
719
  break;
542
720
  }
543
721
  case 'report': {
@@ -558,7 +736,7 @@ async function principal() {
558
736
  pantalla.escribir();
559
737
  pantalla.texto(`**${t('informe.titulo')}**\n\n${inf.texto}`);
560
738
  pantalla.escribir();
561
- pantalla.cierre(`${copiarAlPortapapeles(inf.texto) ? t('informe.copiado') : t('informe.titulo')}${c.gris(sep())}${c.gris(acciones(inf.acciones))}`);
739
+ pantalla.cierre(`${copiarAlPortapapeles(textoParaPegar(inf.texto)) ? t('informe.copiado') : t('informe.titulo')}${c.gris(sep())}${c.gris(acciones(inf.acciones))}`);
562
740
  }
563
741
  catch (e) {
564
742
  pantalla.pararEspera();
@@ -602,8 +780,11 @@ async function principal() {
602
780
  break;
603
781
  }
604
782
  }
605
- if (vista.estado === 'launched')
783
+ if (vista.estado === 'launched') {
606
784
  pantalla.cierre(t('mision.lanzada'));
785
+ if (vista.mision && vista.url && vista.url !== m.url)
786
+ pantalla.escribir(t('mision.aqui', { url: c.marca(vista.url) }));
787
+ }
607
788
  else if (vista.estado === 'failed')
608
789
  pantalla.error(t('mision.no_disponible'));
609
790
  }
@@ -626,6 +807,17 @@ async function principal() {
626
807
  }
627
808
  break;
628
809
  }
810
+ case 'inbox':
811
+ await bandeja();
812
+ superficie?.pintar();
813
+ break;
814
+ case 'mouse': {
815
+ const si = ALIAS_SI_NO[arg] ?? (superficie?.conRaton ? 'no' : 'si');
816
+ guardarAjustes({ raton: si === 'si' });
817
+ superficie?.fijarRaton(si === 'si');
818
+ pantalla.escribir(si === 'si' ? t('raton.on') : t('raton.off'));
819
+ break;
820
+ }
629
821
  case 'new':
630
822
  sesion.conversacion = [];
631
823
  pantalla.escribir(t('repl.nueva'));
@@ -666,11 +858,9 @@ async function principal() {
666
858
  }
667
859
  else {
668
860
  const todos = CONSEJOS.map((id) => consejo(id));
669
- const ancho = Math.max(...todos.map((k) => anchoDe(k.tecla))) + 3;
670
- const filas = todos.map((k) => ` ${c.marca(k.tecla + ' '.repeat(Math.max(0, ancho - anchoDe(k.tecla))))}${k.texto}`);
671
861
  const apagados = leerAjustes().consejos?.apagados;
672
862
  const pie = apagados ? t('consejos.apagados', { orden: ordenDeConsejos(true) }) : t('consejos.quitar', { orden: ordenDeConsejos(false) });
673
- pantalla.escribir([c.negrita(t('consejos.titulo')), ...filas, '', c.gris(pie)].join('\n'));
863
+ pantalla.bloque(bloqueDeTabla(todos.map((k) => [k.tecla, k.texto]), { titulo: t('consejos.titulo'), pie, sangria: 2 }));
674
864
  }
675
865
  break;
676
866
  }
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', 'imagen', 'historial', 'pegar', 'deshacer', 'modelo', 'carriles', 'parar', 'cola', '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'];
4
4
  export function consejo(id) {
5
5
  const u = capacidades().unicode;
6
6
  const tecla = {
@@ -21,6 +21,9 @@ export function consejo(id) {
21
21
  saldo: '/balance',
22
22
  cuenta: 'parx account',
23
23
  agents: 'AGENTS.md',
24
+ bash: '!',
25
+ bandeja: '/inbox',
26
+ raton: 'Click',
24
27
  };
25
28
  return { tecla: tecla[id], texto: t(`tip.${id}`) };
26
29
  }