@getparallel/parx 0.4.0 → 0.4.2

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.2 (2026-10-11)
10
+
11
+ - 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.
12
+ - 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).
13
+ - 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.
14
+ - `/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.
15
+ - 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.
16
+ - Ctrl+Z suspends parx like any terminal program; `fg` brings it back.
17
+ - 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.
18
+ - 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.
19
+ - `/teach` explains every step and ends with a short lesson, also with Swift; `/teach <task>` turns it on and does the task.
20
+ - Typing a whole command and Enter runs it (`/teach`, `/notify`, `/tips`, `/folder`) instead of waiting for a value.
21
+ - `/help` and `/tips` wrap in two clean columns at any width, from 40 columns up.
22
+ - Clearer notes: «3 lines», «2 items», «no matches», «… 8 more lines above»; test output shows pass and fail instead of the runner's filler.
23
+ - `/mission` gives the link to the mission itself once it launches.
24
+ - `/report` copies plain text (no backticks or asterisks) that reads well in an email or WhatsApp.
25
+ - «Running tests» and «Building» only for real test and build commands, not for files like `sum.test.js`.
26
+ - With `--lanes` in a narrow window, each lane keeps its task name instead of a lone «…».
27
+ - Everything you type while parx works (or several lines at once) waits in line, in order; before, only the last one was kept.
28
+ - 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.
29
+
30
+ ## 0.4.1 (2026-10-10)
31
+
32
+ - El nombre visible es siempre «parx», nunca «Par» (consejos, avisos, README)
33
+
9
34
  ## 0.4.0 (2026-10-10)
10
35
 
11
36
  - Published on npm as `@getparallel/parx` (install with `npm install -g @getparallel/parx`). The command is still `parx`.
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,17 +107,35 @@ 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,
118
136
  JPEG, GIF or WebP; big ones are shrunk on macOS.
119
137
 
120
- **Par notifies you:** if you switch to another window while parx works, a desktop notification (macOS,
138
+ **parx notifies you:** if you switch to another window while parx works, a desktop notification (macOS,
121
139
  Windows, Linux) with the Parallel sound tells you when it finishes or needs you. Terminals that do
122
140
  not report focus get it only for tasks longer than 45 s. `/notify off` turns it off.
123
141
 
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, 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' },
@@ -83,9 +86,7 @@ export function modoDe(texto) {
83
86
  }
84
87
  export const SIGUIENTE_MODO = { ask: 'auto', auto: 'read', read: 'ask', bypass: 'ask' };
85
88
  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');
89
+ return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_raton')}` });
89
90
  }
90
91
  async function cuentaOError(cred, pantalla) {
91
92
  try {
@@ -256,6 +257,8 @@ async function principal() {
256
257
  const saldoCorto = () => (disponibles == null ? '' : disponibles.toLocaleString('en-US'));
257
258
  const quien = quienEs(cuenta);
258
259
  let avisos = ajustes.avisos !== false;
260
+ const conRaton = !v['no-mouse'] && v.mouse !== false && (Boolean(v.mouse) || ajustes.raton !== false);
261
+ let esperandoEnBandeja = null;
259
262
  let control = new AbortController();
260
263
  let ocupado = false;
261
264
  let superficie = null;
@@ -391,6 +394,7 @@ async function principal() {
391
394
  saldoCorto: saldoCorto(),
392
395
  peligro: sesion.ctx.modo === 'bypass',
393
396
  extra: sesion.ensenar ? 'Teach' : undefined,
397
+ esperando: esperandoEnBandeja ?? 0,
394
398
  });
395
399
  if (interactiva) {
396
400
  superficie = new Superficie({
@@ -404,6 +408,8 @@ async function principal() {
404
408
  guardarAjustes({ modo: sesion.ctx.modo });
405
409
  },
406
410
  alSalir: adios,
411
+ raton: conRaton,
412
+ alBandeja: () => superficie?.escribirYMandar('/inbox'),
407
413
  alPreguntar: (texto) => {
408
414
  if (avisos && superficie?.enfocada === false)
409
415
  avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
@@ -411,6 +417,17 @@ async function principal() {
411
417
  });
412
418
  const s = superficie;
413
419
  process.on('exit', () => s.restaurarDeUrgencia());
420
+ for (const [señal, codigo] of [['SIGTERM', 143], ['SIGHUP', 129]]) {
421
+ process.on(señal, () => {
422
+ try {
423
+ s.terminar();
424
+ }
425
+ catch {
426
+ s.restaurarDeUrgencia();
427
+ }
428
+ process.exit(codigo);
429
+ });
430
+ }
414
431
  pantalla.fijarSuperficie(superficie);
415
432
  const elegidos = elegirConsejos(leerAjustes().consejos);
416
433
  if (elegidos.ids.length)
@@ -428,6 +445,25 @@ async function principal() {
428
445
  }), conCarriles || quiereBypass ? [] : elegidos.ids);
429
446
  if (quiereBypass)
430
447
  await confirmarBypass();
448
+ const mirarBandeja = async () => {
449
+ if (ocupado)
450
+ return;
451
+ try {
452
+ const r = await leerBandeja(cred.servidor, cred.token);
453
+ const n = r.elementos.length;
454
+ if (n !== esperandoEnBandeja) {
455
+ esperandoEnBandeja = n;
456
+ superficie?.pintar();
457
+ }
458
+ }
459
+ catch (e) {
460
+ if (e instanceof ErrorDeParallel && (e.codigo === 'sin_acceso' || e.codigo === 'sesion'))
461
+ clearInterval(relojBandeja);
462
+ }
463
+ };
464
+ const relojBandeja = setInterval(() => void mirarBandeja(), 90_000);
465
+ relojBandeja.unref();
466
+ void mirarBandeja();
431
467
  }
432
468
  const leerLinea = async (ejemplo = null) => {
433
469
  if (superficie)
@@ -457,6 +493,131 @@ async function principal() {
457
493
  }
458
494
  return tareas;
459
495
  }
496
+ const pedirTexto = async (e, opcion) => {
497
+ const obligatorio = opcion === 'cambios' && e.texto_libre;
498
+ const opcional = e.tipo === 'paso' && opcion === 'hecho' && e.texto_libre;
499
+ if (!obligatorio && !opcional)
500
+ return undefined;
501
+ const l = ((await leerLinea(t(obligatorio ? 'bandeja.escribe' : 'bandeja.nota'))) ?? '').trim();
502
+ if (!l && obligatorio)
503
+ return null;
504
+ return l || undefined;
505
+ };
506
+ const bandeja = async () => {
507
+ const leer = async () => {
508
+ ocupado = true;
509
+ control = new AbortController();
510
+ superficie?.trabajar(() => control.abort());
511
+ pantalla.espera(t('bandeja.mirando'), 'ruta', true);
512
+ try {
513
+ const r = await leerBandeja(cred.servidor, cred.token, control.signal);
514
+ esperandoEnBandeja = r.elementos.length;
515
+ return r.elementos;
516
+ }
517
+ catch (e) {
518
+ if (!(e instanceof ErrorDeParallel))
519
+ throw e;
520
+ if (e.codigo === 'sin_acceso')
521
+ pantalla.error(t('bandeja.no_disponible'));
522
+ else if (e.codigo !== 'interrumpido')
523
+ pantalla.error(mensajeDeError(e));
524
+ return null;
525
+ }
526
+ finally {
527
+ pantalla.pararEspera();
528
+ ocupado = false;
529
+ superficie?.descansar();
530
+ }
531
+ };
532
+ let lista = await leer();
533
+ if (!lista)
534
+ return;
535
+ if (!lista.length) {
536
+ pantalla.escribir(t('bandeja.nada'));
537
+ return;
538
+ }
539
+ if (!superficie) {
540
+ for (const e of lista)
541
+ pantalla.escribir(`${e.titulo}${c.gris(`${sep()}${e.mision}`)} ${c.gris(e.url)}`);
542
+ return;
543
+ }
544
+ let elegida = 0;
545
+ for (;;) {
546
+ const i = await superficie.elegir(t('bandeja.titulo'), lista.map((e) => ({ etiqueta: e.titulo, nota: e.mision })), { vertical: true, elegida });
547
+ if (i === null)
548
+ break;
549
+ elegida = i;
550
+ const e = lista[i];
551
+ if (!e.opciones.length) {
552
+ pantalla.escribir(t('bandeja.abrir', { url: c.marca(e.url) }));
553
+ continue;
554
+ }
555
+ const recomendada = Math.max(0, e.opciones.findIndex((o) => o.recomendada));
556
+ 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 });
557
+ if (j === null || j >= e.opciones.length)
558
+ continue;
559
+ const opcion = e.opciones[j];
560
+ const texto = await pedirTexto(e, opcion.id);
561
+ if (texto === null) {
562
+ pantalla.escribir(c.gris(t('bandeja.cancelado')));
563
+ continue;
564
+ }
565
+ ocupado = true;
566
+ control = new AbortController();
567
+ superficie.trabajar(() => control.abort());
568
+ pantalla.espera(t('bandeja.mirando'), 'ruta', true);
569
+ try {
570
+ const r = await responderBandeja(cred.servidor, cred.token, { id: e.id, opcion: opcion.id, ...(texto ? { texto } : {}) }, control.signal);
571
+ registro.apuntar('bandeja', { tipo: e.tipo, opcion: opcion.id.replace(/:.*/, '') });
572
+ const hecho = { approved: 'bandeja.hecho_aprobado', rejected: 'bandeja.hecho_rechazado', revising: 'bandeja.hecho_cambios', done: 'bandeja.hecho_paso', skipped: 'bandeja.hecho_saltado' };
573
+ pantalla.escribir(`${c.ok(SIMBOLOS.hecho)} ${t(hecho[r.estado] ?? 'bandeja.hecho_aprobado')}${c.gris(`${sep()}${e.mision}`)}`);
574
+ }
575
+ catch (err) {
576
+ if (!(err instanceof ErrorDeParallel))
577
+ throw err;
578
+ if (err.codigo === 'servidor' && err.detalle.startsWith('409'))
579
+ pantalla.aviso(t('bandeja.ya'));
580
+ else if (err.codigo === 'sin_acceso')
581
+ pantalla.aviso(t('bandeja.no_existe'));
582
+ else if (err.codigo === 'servidor' && err.detalle.startsWith('429'))
583
+ pantalla.aviso(t('bandeja.despacio'));
584
+ else if (err.codigo !== 'interrumpido')
585
+ pantalla.error(mensajeDeError(err));
586
+ }
587
+ finally {
588
+ pantalla.pararEspera();
589
+ ocupado = false;
590
+ superficie.descansar();
591
+ }
592
+ lista = await leer();
593
+ superficie.pintar();
594
+ if (!lista)
595
+ return;
596
+ if (!lista.length) {
597
+ pantalla.escribir(t('bandeja.todo'));
598
+ return;
599
+ }
600
+ elegida = Math.min(elegida, lista.length - 1);
601
+ }
602
+ };
603
+ const bash = async (comando) => {
604
+ if (!comando)
605
+ return;
606
+ control = new AbortController();
607
+ try {
608
+ const r = await ejecutarBash(comando, sesion.ctx, control.signal, () => {
609
+ ocupado = true;
610
+ superficie?.trabajar(() => control.abort());
611
+ superficie?.textoDeTrabajo(t('bash.corriendo', { comando: comando.length > 28 ? `${comando.slice(0, 27)}…` : comando }), 'tren', true);
612
+ });
613
+ if (r)
614
+ sesion.conversacion.push({ role: 'user', content: textoParaElAgente(comando, r) });
615
+ }
616
+ finally {
617
+ ocupado = false;
618
+ superficie?.descansar();
619
+ }
620
+ };
460
621
  for (;;) {
461
622
  const r = await leerLinea();
462
623
  if (r === null)
@@ -465,6 +626,10 @@ async function principal() {
465
626
  ctrlC = 0;
466
627
  if (!linea)
467
628
  continue;
629
+ if (linea.startsWith('!')) {
630
+ await bash(linea.slice(1).trim());
631
+ continue;
632
+ }
468
633
  if (linea.startsWith('/')) {
469
634
  const [nombre = '', ...args] = linea.slice(1).split(/\s+/);
470
635
  const cmd = ALIAS_COMANDO[nombre.toLowerCase()];
@@ -479,7 +644,7 @@ async function principal() {
479
644
  rl?.close();
480
645
  return 0;
481
646
  case 'help':
482
- pantalla.escribir(ayudaDeComandos());
647
+ pantalla.bloque(ayudaDeComandos());
483
648
  break;
484
649
  case 'mode': {
485
650
  const m = modoDe(arg);
@@ -534,10 +699,18 @@ async function principal() {
534
699
  break;
535
700
  }
536
701
  case 'teach': {
537
- const si = ALIAS_SI_NO[arg] ?? (sesion.ensenar ? 'no' : 'si');
702
+ const tareaConClase = !ALIAS_SI_NO[arg] && resto ? resto : null;
703
+ const si = tareaConClase ? 'si' : (ALIAS_SI_NO[arg] ?? (sesion.ensenar ? 'no' : 'si'));
704
+ const cambia = sesion.ensenar !== (si === 'si');
538
705
  sesion.ensenar = si === 'si';
539
- pantalla.escribir(sesion.ensenar ? t('repl.ensenar_on') : t('repl.ensenar_off'));
706
+ if (cambia || !tareaConClase)
707
+ pantalla.escribir(sesion.ensenar ? t('repl.ensenar_on') : t('repl.ensenar_off'));
540
708
  superficie?.pintar();
709
+ if (tareaConClase && !(await hacer(tareaConClase))) {
710
+ superficie?.terminar(despedida());
711
+ rl?.close();
712
+ return 1;
713
+ }
541
714
  break;
542
715
  }
543
716
  case 'report': {
@@ -558,7 +731,7 @@ async function principal() {
558
731
  pantalla.escribir();
559
732
  pantalla.texto(`**${t('informe.titulo')}**\n\n${inf.texto}`);
560
733
  pantalla.escribir();
561
- pantalla.cierre(`${copiarAlPortapapeles(inf.texto) ? t('informe.copiado') : t('informe.titulo')}${c.gris(sep())}${c.gris(acciones(inf.acciones))}`);
734
+ pantalla.cierre(`${copiarAlPortapapeles(textoParaPegar(inf.texto)) ? t('informe.copiado') : t('informe.titulo')}${c.gris(sep())}${c.gris(acciones(inf.acciones))}`);
562
735
  }
563
736
  catch (e) {
564
737
  pantalla.pararEspera();
@@ -602,8 +775,11 @@ async function principal() {
602
775
  break;
603
776
  }
604
777
  }
605
- if (vista.estado === 'launched')
778
+ if (vista.estado === 'launched') {
606
779
  pantalla.cierre(t('mision.lanzada'));
780
+ if (vista.mision && vista.url && vista.url !== m.url)
781
+ pantalla.escribir(t('mision.aqui', { url: c.marca(vista.url) }));
782
+ }
607
783
  else if (vista.estado === 'failed')
608
784
  pantalla.error(t('mision.no_disponible'));
609
785
  }
@@ -626,6 +802,17 @@ async function principal() {
626
802
  }
627
803
  break;
628
804
  }
805
+ case 'inbox':
806
+ await bandeja();
807
+ superficie?.pintar();
808
+ break;
809
+ case 'mouse': {
810
+ const si = ALIAS_SI_NO[arg] ?? (superficie?.conRaton ? 'no' : 'si');
811
+ guardarAjustes({ raton: si === 'si' });
812
+ superficie?.fijarRaton(si === 'si');
813
+ pantalla.escribir(si === 'si' ? t('raton.on') : t('raton.off'));
814
+ break;
815
+ }
629
816
  case 'new':
630
817
  sesion.conversacion = [];
631
818
  pantalla.escribir(t('repl.nueva'));
@@ -666,11 +853,9 @@ async function principal() {
666
853
  }
667
854
  else {
668
855
  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
856
  const apagados = leerAjustes().consejos?.apagados;
672
857
  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'));
858
+ pantalla.bloque(bloqueDeTabla(todos.map((k) => [k.tecla, k.texto]), { titulo: t('consejos.titulo'), pie, sangria: 2 }));
674
859
  }
675
860
  break;
676
861
  }
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
  }