@getparallel/parx 0.4.3 → 0.4.4

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,13 @@ 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.4 (2026-10-11)
10
+
11
+ - 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`.
12
+ - 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`.
13
+ - `/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.
14
+ - `/mouse off` is remembered in the next session too (it was saved but not read back).
15
+
9
16
  ## 0.4.3 (2026-10-11)
10
17
 
11
18
  - 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,10 +93,19 @@ when it asks first:
93
93
  ## Inside a session
94
94
 
95
95
  The input sits at the bottom from the start; the status lives in one place, the row under the rail:
96
- the mode and the model at rest; while it works, what it is doing in plain words («Reading 3 files»,
97
- «Running tests», or «Tracing the route…» while it plans), the time and the Actions so far, with a
98
- small rail animation (a dot along the route, a train on the track, a sleeper scanning the rails, the
99
- track being laid).
96
+ the mode and the model at rest; while it works, it always says what it is doing right now, with a
97
+ small rail animation in violet, the time and the Actions so far. It is never silent: «Thinking…»
98
+ while Parallel thinks, then the real step as it happens («Reading src/app.ts…», «Searching for
99
+ useAuth…», «Editing 2 files…», «Running tests…», «Running git status…», «Writing the answer…»). If
100
+ the server takes a while, only what is really going on («Planning the steps…», «Looking at the
101
+ results…», «Waiting for the server…»). It works the same in `--lanes` (one row per lane), at 40
102
+ columns, with `NO_COLOR` and with `--ascii`.
103
+
104
+ **The answer is written out**, word by word, with a soft cursor, in 2 seconds at most (long answers
105
+ go faster). It is laid out whole first, so markdown and code blocks end exactly as if they had
106
+ appeared at once, and scrolling up while it writes does not move what you are reading. Any key shows
107
+ the rest at once. `/animation off` (saved) or `--no-animation` shows answers whole; without a
108
+ terminal (a pipe, CI), in `--lanes` and in `/report` they are always whole.
100
109
 
101
110
  Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or Enter pick, Esc closes.
102
111
 
@@ -110,10 +119,12 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
110
119
  | `/inbox` | what your missions are waiting for (approvals, proposed changes, your steps), as buttons; what you pick takes effect like on the web |
111
120
  | `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
112
121
  | `/teach` | class mode: explains each step as it codes, like a teacher |
113
- | `/undo`, `/new`, `/balance`, `/log`, `/help`, `/exit` | the usual |
122
+ | `/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 |
123
+ | `/undo`, `/balance`, `/log`, `/help`, `/exit` | the usual |
114
124
  | `/notify` on, off | desktop notifications |
115
125
  | `/tips` on, off | the welcome tips |
116
126
  | `/mouse` on, off | clicks and wheel inside parx (saved; `--no-mouse` for one session) |
127
+ | `/animation` on, off | answers written out as they arrive, or whole (saved; `--no-animation` for one session) |
117
128
 
118
129
  **Bash mode:** start the input with `!` and it turns amber with a small `bash` label; Enter runs the
119
130
  command in your shell, in the project folder, and its output streams into the conversation with the
@@ -0,0 +1,131 @@
1
+ import { capacidades, c } from "./marca.js";
2
+ import { t } from "./textos.js";
3
+ import { anchoDeCaracter, sinColor } from "./texto.js";
4
+ export function elipsis() {
5
+ return capacidades().unicode ? '…' : '...';
6
+ }
7
+ export function conElipsis(texto) {
8
+ const s = texto.trimEnd();
9
+ if (!s || /(…|\.\.\.|[.!?:])$/.test(s))
10
+ return s;
11
+ return `${s}${elipsis()}`;
12
+ }
13
+ const AL_EMPEZAR = [
14
+ [0, 'trabajo.pensando'],
15
+ [6_000, 'trabajo.planear'],
16
+ [15_000, 'trabajo.sigue'],
17
+ [35_000, 'trabajo.servidor'],
18
+ ];
19
+ const TRAS_HERRAMIENTAS = [
20
+ [0, 'trabajo.pensando'],
21
+ [5_000, 'trabajo.resultados'],
22
+ [11_000, 'trabajo.siguiente'],
23
+ [20_000, 'trabajo.sigue'],
24
+ [40_000, 'trabajo.servidor'],
25
+ ];
26
+ export function fraseDeEspera(ms, trasHerramientas = false) {
27
+ const tabla = trasHerramientas ? TRAS_HERRAMIENTAS : AL_EMPEZAR;
28
+ let clave = tabla[0][1];
29
+ for (const [desde, k] of tabla)
30
+ if (Math.max(0, ms) >= desde)
31
+ clave = k;
32
+ return conElipsis(t(clave));
33
+ }
34
+ export const CARACTERES_POR_SEGUNDO = 110;
35
+ export const MS_MAX_RESPUESTA = 2000;
36
+ export const MS_MAX_INTERMEDIO = 900;
37
+ export const MS_POR_FOTOGRAMA = 33;
38
+ export function duracionDeRevelado(caracteres, maxMs = MS_MAX_RESPUESTA) {
39
+ if (caracteres <= 0)
40
+ return 0;
41
+ return Math.round(Math.min(maxMs, Math.max(160, (caracteres / CARACTERES_POR_SEGUNDO) * 1000)));
42
+ }
43
+ export function caracteresVisibles(total, ms, duracion) {
44
+ if (duracion <= 0 || ms >= duracion)
45
+ return total;
46
+ return Math.max(0, Math.min(total, Math.floor((total * Math.max(0, ms)) / duracion)));
47
+ }
48
+ export function cursorEncendido(ms) {
49
+ return Math.floor(Math.max(0, ms) / 500) % 2 === 0;
50
+ }
51
+ export function simboloDeCursor() {
52
+ return c.violeta(capacidades().unicode ? '▍' : '_');
53
+ }
54
+ export function caracteresDe(lineas) {
55
+ let n = 0;
56
+ for (const l of lineas)
57
+ n += anchoVisible(l);
58
+ return n;
59
+ }
60
+ function anchoVisible(l) {
61
+ let n = 0;
62
+ for (const ch of sinColor(l))
63
+ n += anchoDeCaracter(ch);
64
+ return n;
65
+ }
66
+ const ESCAPE = /\x1b\[[0-9;?]*[ -/]*[@-~]/y;
67
+ export function cortarVisible(linea, n) {
68
+ if (n <= 0)
69
+ return '';
70
+ let out = '';
71
+ let w = 0;
72
+ let color = false;
73
+ let i = 0;
74
+ while (i < linea.length) {
75
+ if (linea[i] === '\x1b') {
76
+ ESCAPE.lastIndex = i;
77
+ const m = ESCAPE.exec(linea);
78
+ if (m) {
79
+ out += m[0];
80
+ color = true;
81
+ i += m[0].length;
82
+ continue;
83
+ }
84
+ }
85
+ const ch = String.fromCodePoint(linea.codePointAt(i));
86
+ const cw = anchoDeCaracter(ch);
87
+ if (w + cw > n)
88
+ return `${out}${color ? '\x1b[0m' : ''}`;
89
+ out += ch;
90
+ w += cw;
91
+ i += ch.length;
92
+ }
93
+ return out;
94
+ }
95
+ export function revelarLineas(lineas, n, cursor, ancho = Infinity) {
96
+ const total = caracteresDe(lineas);
97
+ if (n >= total)
98
+ return cursor ? conCursor([...lineas], cursor, ancho) : [...lineas];
99
+ const out = [];
100
+ let quedan = Math.max(0, n);
101
+ for (const l of lineas) {
102
+ const w = anchoVisible(l);
103
+ if (quedan >= w) {
104
+ out.push(l);
105
+ quedan -= w;
106
+ continue;
107
+ }
108
+ const plano = Array.from(sinColor(l));
109
+ let cols = 0;
110
+ let k = 0;
111
+ while (k < plano.length && cols < quedan)
112
+ cols += anchoDeCaracter(plano[k++]);
113
+ while (k < plano.length && plano[k] !== ' ')
114
+ cols += anchoDeCaracter(plano[k++]);
115
+ const parte = cortarVisible(l, cols);
116
+ out.push(sinColor(parte).trim() ? parte : '');
117
+ break;
118
+ }
119
+ while (out.length > 1 && out[out.length - 1] === '' && lineas[out.length - 1] !== '')
120
+ out.pop();
121
+ return cursor ? conCursor(out, cursor, ancho) : out;
122
+ }
123
+ function conCursor(lineas, cursor, ancho) {
124
+ if (!lineas.length)
125
+ return [cursor];
126
+ const i = lineas.length - 1;
127
+ const l = lineas[i];
128
+ if (anchoVisible(l) + 1 < ancho)
129
+ lineas[i] = `${l.replace(/\s+$/, '')}${anchoVisible(l) ? ' ' : ''}${cursor}`;
130
+ return lineas;
131
+ }
package/dist/agente.js CHANGED
@@ -153,29 +153,31 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
153
153
  disponibles = r.acciones.disponibles;
154
154
  pantalla.progreso(acciones);
155
155
  registro.apuntar('turno', { tarea, vuelta, acciones });
156
- const llamadas = [];
156
+ const llamadas = r.salida.filter((i) => i.type === 'function_call');
157
157
  for (const item of r.salida) {
158
158
  s.conversacion.push(item);
159
159
  if (item.type === 'message') {
160
160
  const texto = textoDeMensaje(item).trim();
161
161
  if (texto) {
162
162
  respuesta = texto;
163
+ pantalla.espera(t('hace.respuesta'), 'tendido', true);
163
164
  pantalla.escribir();
164
- pantalla.texto(texto);
165
+ await pantalla.textoAnimado(texto, { intermedio: llamadas.length > 0 });
165
166
  pantalla.escribir();
167
+ pantalla.espera(null);
166
168
  }
167
169
  }
168
- else if (item.type === 'function_call')
169
- llamadas.push(item);
170
170
  }
171
171
  if (!llamadas.length)
172
172
  break;
173
- const etiqueta = etiquetaDeTanda(llamadas.map((l) => ({ name: String(l.name ?? ''), arguments: String(l.arguments ?? '{}') })));
174
- if (etiqueta)
175
- pantalla.espera(etiqueta.texto, etiqueta.anim, false);
176
- for (const ll of llamadas) {
173
+ const todas = llamadas.map((l) => ({ name: String(l.name ?? ''), arguments: String(l.arguments ?? '{}') }));
174
+ for (let i = 0; i < llamadas.length; i++) {
175
+ const ll = llamadas[i];
177
176
  if (señal.aborted)
178
177
  throw new ErrorDeParallel('interrumpido');
178
+ const etiqueta = etiquetaDeTanda(grupoDesde(todas, i));
179
+ if (etiqueta && empiezaGrupo(todas, i))
180
+ pantalla.espera(etiqueta.texto, etiqueta.anim, false);
179
181
  const salida = await ejecutarHerramienta(String(ll.name ?? ''), String(ll.arguments ?? '{}'), s.ctx);
180
182
  s.conversacion.push({ type: 'function_call_output', call_id: ll.call_id, output: salida });
181
183
  }
@@ -209,6 +211,31 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
209
211
  pantalla.cierre(`${t('estado.hecho')}${sep}${c.gris(textoDeAcciones(acciones))}${sep}${c.gris(duracion(ms))}`);
210
212
  return true;
211
213
  }
214
+ function familia(nombre) {
215
+ if (nombre === 'write_file' || nombre === 'edit_file')
216
+ return 'cambio';
217
+ if (nombre === 'read_file' || nombre === 'search' || nombre === 'list_files')
218
+ return nombre;
219
+ return null;
220
+ }
221
+ export function grupoDesde(llamadas, i) {
222
+ const actual = llamadas[i];
223
+ if (!actual)
224
+ return [];
225
+ const f = familia(actual.name);
226
+ if (!f)
227
+ return [actual];
228
+ let j = i;
229
+ while (j < llamadas.length && familia(llamadas[j].name) === f)
230
+ j++;
231
+ return llamadas.slice(i, j);
232
+ }
233
+ export function empiezaGrupo(llamadas, i) {
234
+ if (i === 0)
235
+ return true;
236
+ const f = familia(llamadas[i].name);
237
+ return !f || f !== familia(llamadas[i - 1].name);
238
+ }
212
239
  function cerrarLlamadasPendientes(items) {
213
240
  const respondidas = new Set(items.filter((i) => i.type === 'function_call_output').map((i) => i.call_id));
214
241
  for (const i of [...items]) {
package/dist/cli.js CHANGED
@@ -43,6 +43,8 @@ function opciones() {
43
43
  ascii: { type: 'boolean' },
44
44
  mouse: { type: 'boolean' },
45
45
  'no-mouse': { type: 'boolean' },
46
+ 'no-animation': { type: 'boolean' },
47
+ animation: { type: 'boolean' },
46
48
  version: { type: 'boolean', short: 'v' },
47
49
  help: { type: 'boolean', short: 'h' },
48
50
  browser: { type: 'boolean' },
@@ -262,10 +264,40 @@ async function principal() {
262
264
  const quien = quienEs(cuenta);
263
265
  let avisos = ajustes.avisos !== false;
264
266
  const conRaton = !v['no-mouse'] && v.mouse !== false && (Boolean(v.mouse) || ajustes.raton !== false);
267
+ pantalla.animar = !v['no-animation'] && v.animation !== false && (Boolean(v.animation) || ajustes.animacion !== false);
265
268
  let esperandoEnBandeja = null;
266
269
  let control = new AbortController();
267
270
  let ocupado = false;
268
271
  let superficie = null;
272
+ let bienvenida;
273
+ let consejosDeEntrada = [];
274
+ const confirmarNuevaEnMarcha = async () => {
275
+ if (!superficie)
276
+ return;
277
+ 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 });
278
+ if (i !== 0)
279
+ return;
280
+ superficie.nuevaTrasParar();
281
+ if (ocupado)
282
+ control.abort();
283
+ };
284
+ const empezarDeNuevo = () => {
285
+ sesion.conversacion = [];
286
+ sesion.diario = [];
287
+ sesion.id = randomUUID();
288
+ sesion.ultimaTarea = undefined;
289
+ sesion.ctx.hechos = { ficheros: [], comandos: [] };
290
+ sesion.ctx.permitidos.clear();
291
+ sesion.ctx.deshacer.length = 0;
292
+ if (sesion.ctx.lecturas)
293
+ sesion.ctx.lecturas.length = 0;
294
+ registro.apuntar('nueva', {});
295
+ pantalla.reiniciar();
296
+ if (superficie)
297
+ superficie.reiniciar(bienvenida, consejosDeEntrada);
298
+ else
299
+ pantalla.escribir(t('repl.nueva'));
300
+ };
269
301
  let ctrlC = 0;
270
302
  const totalDeLaSesion = () => sesion.total.tareas ? c.gris(t('estado.total', { acciones: acciones(sesion.total.acciones), tiempo: duracion(sesion.total.ms) })) : '';
271
303
  const despedida = () => [totalDeLaSesion(), c.gris(t('repl.adios'))].filter(Boolean).join('\n ');
@@ -419,6 +451,7 @@ async function principal() {
419
451
  if (avisos && superficie?.enfocada === false)
420
452
  avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
421
453
  },
454
+ alNuevaEnMarcha: () => void confirmarNuevaEnMarcha(),
422
455
  });
423
456
  const s = superficie;
424
457
  process.on('exit', () => s.restaurarDeUrgencia());
@@ -437,7 +470,8 @@ async function principal() {
437
470
  const elegidos = elegirConsejos(leerAjustes().consejos);
438
471
  if (elegidos.ids.length)
439
472
  guardarAjustes({ consejos: elegidos.estado });
440
- superficie.iniciar(bloqueDeBienvenida({
473
+ consejosDeEntrada = conCarriles || quiereBypass ? [] : elegidos.ids;
474
+ bienvenida = bloqueDeBienvenida({
441
475
  version: VERSION,
442
476
  quien: quien.includes('@') ? quien : (quien.split(/\s+/)[0] ?? ''),
443
477
  carpeta: rutaCorta(raiz, 60),
@@ -447,7 +481,8 @@ async function principal() {
447
481
  return nueva ? t('update.disponible', { version: nueva }) : undefined;
448
482
  })(),
449
483
  peligro: () => sesion.ctx.modo === 'bypass',
450
- }), conCarriles || quiereBypass ? [] : elegidos.ids);
484
+ });
485
+ superficie.iniciar(bienvenida, consejosDeEntrada);
451
486
  if (quiereBypass)
452
487
  await confirmarBypass();
453
488
  const mirarBandeja = async () => {
@@ -819,9 +854,15 @@ async function principal() {
819
854
  break;
820
855
  }
821
856
  case 'new':
822
- sesion.conversacion = [];
823
- pantalla.escribir(t('repl.nueva'));
857
+ empezarDeNuevo();
858
+ break;
859
+ case 'animation': {
860
+ const si = ALIAS_SI_NO[arg] ?? (pantalla.animar ? 'no' : 'si');
861
+ pantalla.animar = si === 'si';
862
+ guardarAjustes({ animacion: pantalla.animar });
863
+ pantalla.escribir(pantalla.animar ? t('animacion.on') : t('animacion.off'));
824
864
  break;
865
+ }
825
866
  case 'undo': {
826
867
  const r2 = deshacerUltimo(sesion.ctx);
827
868
  pantalla.escribir(r2 ? t('repl.deshecho', { ruta: r2 }) : t('repl.nada_que_deshacer'));
package/dist/config.js CHANGED
@@ -92,6 +92,10 @@ export function leerAjustes(dir = carpetaDeConfiguracion()) {
92
92
  out.modo = MODO_VIEJO[a.modo];
93
93
  if (typeof a.avisos === 'boolean')
94
94
  out.avisos = a.avisos;
95
+ if (typeof a.raton === 'boolean')
96
+ out.raton = a.raton;
97
+ if (typeof a.animacion === 'boolean')
98
+ out.animacion = a.animacion;
95
99
  const k = a.consejos;
96
100
  if (k && typeof k === 'object') {
97
101
  const n = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
@@ -3,6 +3,7 @@ import { dirname, join, relative } from 'node:path';
3
3
  import { contarCambios, diffDeLineas } from "./diff.js";
4
4
  import { cuantos, t } from "./textos.js";
5
5
  import { c } from "./marca.js";
6
+ import { elipsis } from "./actividad.js";
6
7
  import { estaDentro, familiaDeComando, nivelDeComando, resolverRuta, rutaReal } from "./seguridad.js";
7
8
  import { ejecutarComando, recortar } from "./shell.js";
8
9
  const nulable = (tipo, descripcion) => ({ type: [tipo, 'null'], description: descripcion });
@@ -211,7 +212,15 @@ export function etiquetaDeComando(comando) {
211
212
  return t('hace.instalar');
212
213
  const primera = c1.split(/\s*(?:&&|\|\||;|\|)\s*/)[0] ?? c1;
213
214
  const corto = primera.split(/\s+/).filter((p) => !/^[A-Z_]+=/.test(p)).slice(0, 3).join(' ');
214
- return t('hace.ejecutar', { comando: corto.length > 28 ? `${corto.slice(0, 27)}…` : corto });
215
+ return t('hace.ejecutar', { comando: corto.length > 28 ? `${corto.slice(0, 27)}${elipsis()}` : corto });
216
+ }
217
+ export function rutaParaLaBarra(ruta) {
218
+ const r = ruta.trim().replace(/^\.\/(?=.)/, '') || '.';
219
+ if (r.length <= 30)
220
+ return r;
221
+ const partes = r.split(/[\\/]/).filter(Boolean);
222
+ const cola = partes.slice(-2).join('/');
223
+ return cola.length <= 30 ? `${elipsis()}/${cola}` : `${elipsis()}/${partes[partes.length - 1]}`;
215
224
  }
216
225
  export function etiquetaDeTanda(llamadas) {
217
226
  if (!llamadas.length)
@@ -225,7 +234,7 @@ export function etiquetaDeTanda(llamadas) {
225
234
  }
226
235
  });
227
236
  const nombres = llamadas.map((l) => l.name);
228
- const base = (p) => String(p ?? '.').split(/[\\/]/).filter(Boolean).pop() ?? '.';
237
+ const base = (p) => rutaParaLaBarra(String(p ?? '.'));
229
238
  const cmd = nombres.indexOf('run_command');
230
239
  if (cmd !== -1)
231
240
  return { texto: etiquetaDeComando(String(args[cmd].command ?? '')), anim: 'tren' };
@@ -244,9 +253,14 @@ export function etiquetaDeTanda(llamadas) {
244
253
  const b = nombres.indexOf('search');
245
254
  if (b !== -1) {
246
255
  const patron = String(args[b].pattern ?? '');
247
- return { texto: t('hace.buscar', { patron: patron.length > 24 ? `${patron.slice(0, 23)}…` : patron }), anim: 'lectura' };
256
+ if (!patron.trim())
257
+ return { texto: t('hace.buscar_ficheros'), anim: 'lectura' };
258
+ return { texto: t('hace.buscar', { patron: patron.length > 24 ? `${patron.slice(0, 23)}${elipsis()}` : patron }), anim: 'lectura' };
248
259
  }
249
- return { texto: t('hace.listar', { ruta: base(args[0].path ?? '.') }), anim: 'lectura' };
260
+ const l = nombres.indexOf('list_files');
261
+ if (l !== -1)
262
+ return { texto: t('hace.listar', { ruta: base(args[l].path ?? '.') }), anim: 'lectura' };
263
+ return { texto: t('hace.generico'), anim: 'ruta' };
250
264
  }
251
265
  export async function ejecutarHerramienta(nombre, argumentos, ctx) {
252
266
  let a;
package/dist/textos.js CHANGED
@@ -19,7 +19,7 @@ export const TEXTOS = {
19
19
  'tui.si_ediciones': 'Yes, allow all edits',
20
20
  'tui.si_ediciones_corto': 'All edits',
21
21
  'tui.una_vez': 'Yes, this once',
22
- 'tui.atajos': 'Shift+Tab mode · ↑ ↓ history · ! runs a shell command · Esc stops a task · Ctrl+C twice exits · Ctrl+L redraws',
22
+ 'tui.atajos': 'Shift+Tab mode · ↑ ↓ history · ! runs a shell command · Esc stops a task · any key shows the whole answer · Ctrl+C twice exits · Ctrl+L redraws',
23
23
  'tui.atajos_raton': 'Click any option or button · wheel or PgUp/PgDn scrolls · End jumps to the bottom · Shift+drag selects text (Option in iTerm2, Fn in Terminal) · /mouse off gives the mouse back to the terminal',
24
24
  'tui.al_fondo': 'Jump to bottom',
25
25
  'tui.nuevas': '{n} new',
@@ -76,21 +76,22 @@ export const TEXTOS = {
76
76
  'carpeta.lista': 'Allowed folders: {lista}',
77
77
  'carpeta.ninguna': 'Only the project folder. Add one with /folder <path>.',
78
78
  'carpeta.no_existe': 'That folder does not exist: {ruta}',
79
- 'trabajo.ruta': 'Tracing the route',
80
- 'trabajo.carriles': 'On the rails',
81
- 'trabajo.puntos': 'Connecting the dots',
82
- 'trabajo.numeros': 'Running the numbers',
83
- 'trabajo.afinar': 'Fine-tuning',
84
- 'trabajo.agujas': 'Switching tracks',
85
- 'trabajo.mapa': 'Reading the map',
86
- 'trabajo.estacion': 'Nearing the station',
79
+ 'trabajo.pensando': 'Thinking',
80
+ 'trabajo.planear': 'Planning the steps',
81
+ 'trabajo.resultados': 'Looking at the results',
82
+ 'trabajo.siguiente': 'Deciding the next step',
83
+ 'trabajo.sigue': 'Still thinking',
84
+ 'trabajo.servidor': 'Waiting for the server',
85
+ 'hace.respuesta': 'Writing the answer',
86
+ 'hace.generico': 'Working',
87
+ 'hace.buscar_ficheros': 'Searching files',
87
88
  'hace.leer': 'Reading {n} files',
88
89
  'hace.leer_uno': 'Reading {ruta}',
89
90
  'hace.listar': 'Looking around {ruta}',
90
91
  'hace.buscar': 'Searching for {patron}',
91
92
  'hace.escribir': 'Writing {ruta}',
92
93
  'hace.editar': 'Editing {ruta}',
93
- 'hace.cambios': 'Changing {n} files',
94
+ 'hace.cambios': 'Editing {n} files',
94
95
  'hace.tests': 'Running tests',
95
96
  'hace.build': 'Building',
96
97
  'hace.instalar': 'Installing',
@@ -181,6 +182,12 @@ export const TEXTOS = {
181
182
  'repl.adios': 'See you.',
182
183
  'repl.registro': 'Log of this session: {ruta}',
183
184
  'repl.nueva': 'New conversation.',
185
+ 'nueva.parar': 'Stop and start new?',
186
+ 'nueva.parar_detalle': 'parx is working. This stops it and clears the conversation.',
187
+ 'nueva.si': 'Stop and start new',
188
+ 'nueva.no': 'Keep working',
189
+ 'animacion.on': 'Answers are written out as they arrive. Any key shows the rest at once.',
190
+ 'animacion.off': 'Answers appear whole, at once. /animation on to turn it back on.',
184
191
  'repl.desconocido': 'Unknown command. Type /help.',
185
192
  'repl.deshecho': 'Undone: {ruta}',
186
193
  'repl.nada_que_deshacer': 'Nothing to undo.',
@@ -261,6 +268,7 @@ export const COMANDOS = [
261
268
  ['notify', 'Desktop notifications: on or off'],
262
269
  ['tips', 'Tips when it opens: on or off'],
263
270
  ['mouse', 'Clicks and wheel in parx: on or off'],
271
+ ['animation', 'Answers written out as they arrive: on or off'],
264
272
  ['log', 'Where the log of what it does is kept'],
265
273
  ['exit', 'Leave parx'],
266
274
  ];
@@ -273,6 +281,7 @@ export const ALIAS_COMANDO = {
273
281
  mission: 'mission', mision: 'mission', misión: 'mission',
274
282
  inbox: 'inbox', bandeja: 'inbox', approvals: 'inbox', waiting: 'inbox',
275
283
  mouse: 'mouse', raton: 'mouse', ratón: 'mouse',
284
+ animation: 'animation', animations: 'animation', animacion: 'animation', animación: 'animation',
276
285
  report: 'report', informe: 'report',
277
286
  teach: 'teach', class: 'teach', clase: 'teach',
278
287
  undo: 'undo', deshacer: 'undo',
@@ -304,6 +313,7 @@ export const USO = [
304
313
  ' --dir <folder> the project folder (default: current)',
305
314
  ' --ascii plain characters only (also NO_COLOR for no color)',
306
315
  ' --no-mouse leave the mouse to the terminal (no clicks or wheel in parx)',
316
+ ' --no-animation show answers whole, without writing them out',
307
317
  ' --version, --help',
308
318
  '',
309
319
  'Sign in:',
package/dist/tui.js CHANGED
@@ -4,6 +4,7 @@ import { ALIAS_COMANDO, COMANDOS, t as tt } from "./textos.js";
4
4
  import { MODELOS, ORDEN_DE_MODELOS } from "./modelos.js";
5
5
  import { consejo, ordenDeConsejos } from "./consejos.js";
6
6
  export { anchoDe } from "./texto.js";
7
+ import { conElipsis, fraseDeEspera } from "./actividad.js";
7
8
  export const MARGEN = 2;
8
9
  export function anchoUtil(ancho) {
9
10
  return Math.max(10, ancho - MARGEN * 2);
@@ -287,9 +288,9 @@ function teclaDeRaton(b, x, y, soltar, botonX10) {
287
288
  }
288
289
  export const RATON_SI = '\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h';
289
290
  export const RATON_NO = '\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l';
290
- const CON_VALOR = new Set(['mode', 'model', 'tips', 'notify', 'teach', 'mouse']);
291
+ const CON_VALOR = new Set(['mode', 'model', 'tips', 'notify', 'teach', 'mouse', 'animation']);
291
292
  const CON_TEXTO = new Set(['folder']);
292
- const SE_MANDAN_SOLOS = new Set(['teach', 'notify', 'tips', 'folder', 'mouse']);
293
+ const SE_MANDAN_SOLOS = new Set(['teach', 'notify', 'tips', 'folder', 'mouse', 'animation']);
293
294
  const MODOS_DEL_MENU = ['ask', 'auto', 'read', 'bypass'];
294
295
  export function opcionesDelMenu(valor) {
295
296
  if (!valor.startsWith('/') || valor.includes('\n'))
@@ -321,8 +322,8 @@ export function opcionesDelMenu(valor) {
321
322
  : cmd === 'model'
322
323
  ? ORDEN_DE_MODELOS.map((k) => [k, `${MODELOS[k].nombre} ${MODELOS[k].version}: ${MODELOS[k].descripcion}`])
323
324
  : [
324
- ['on', cmd === 'teach' ? 'Explain each step as it codes' : cmd === 'notify' ? 'Notify me when it finishes or needs me' : cmd === 'mouse' ? 'Click options, scroll with the wheel' : 'Show them when parx opens'],
325
- ['off', cmd === 'teach' ? 'Back to normal' : cmd === 'notify' ? 'No desktop notifications' : cmd === 'mouse' ? 'The terminal selects and scrolls as usual' : 'Don’t show them again'],
325
+ ['on', cmd === 'teach' ? 'Explain each step as it codes' : cmd === 'notify' ? 'Notify me when it finishes or needs me' : cmd === 'mouse' ? 'Click options, scroll with the wheel' : cmd === 'animation' ? 'Write answers out as they arrive' : 'Show them when parx opens'],
326
+ ['off', cmd === 'teach' ? 'Back to normal' : cmd === 'notify' ? 'No desktop notifications' : cmd === 'mouse' ? 'The terminal selects and scrolls as usual' : cmd === 'animation' ? 'Show answers whole, at once' : 'Don’t show them again'],
326
327
  ];
327
328
  return valores
328
329
  .filter(([v]) => v.startsWith(q))
@@ -380,17 +381,11 @@ export function duracion(ms) {
380
381
  export function acciones(n) {
381
382
  return n === 1 ? tt('estado.accion') : tt('estado.acciones', { n: n.toLocaleString('en-US') });
382
383
  }
383
- const FRASES = ['trabajo.ruta', 'trabajo.puntos', 'trabajo.carriles', 'trabajo.numeros', 'trabajo.mapa', 'trabajo.afinar', 'trabajo.agujas', 'trabajo.estacion'];
384
- export const MS_POR_FRASE = 2800;
385
- export function fraseDeEspera(ms, semilla = 0) {
386
- const vuelta = Math.floor(Math.max(0, ms) / MS_POR_FRASE);
387
- if (vuelta === 0)
388
- return `${tt(FRASES[0])}…`;
389
- return `${tt(FRASES[1 + ((semilla + vuelta - 1) % (FRASES.length - 1))])}…`;
390
- }
384
+ export { fraseDeEspera } from "./actividad.js";
391
385
  export function ponerTextoDeTrabajo(w, texto, anim = 'ruta', vivo = false, ahora = Date.now()) {
392
386
  if (texto) {
393
387
  w.texto = texto;
388
+ w.trasHerramientas = true;
394
389
  w.anim = anim;
395
390
  w.vivo = vivo;
396
391
  w.literalHasta = ahora + 1800;
@@ -401,12 +396,12 @@ export function ponerTextoDeTrabajo(w, texto, anim = 'ruta', vivo = false, ahora
401
396
  }
402
397
  }
403
398
  export function nuevoTrabajo(ahora = Date.now()) {
404
- return { texto: null, literalHasta: 0, vivo: false, anim: 'ruta', desde: ahora, esperaDesde: ahora, paso: 0, acciones: null, semilla: Math.floor(Math.random() * 7) };
399
+ return { texto: null, literalHasta: 0, vivo: false, anim: 'ruta', desde: ahora, esperaDesde: ahora, paso: 0, acciones: null, trasHerramientas: false };
405
400
  }
406
401
  export function textoDeTrabajo(w, ahora) {
407
402
  if (w.texto && (w.vivo || ahora < w.literalHasta))
408
- return { texto: w.texto, anim: w.anim };
409
- return { texto: fraseDeEspera(ahora - w.esperaDesde, w.semilla), anim: 'ruta' };
403
+ return { texto: conElipsis(w.texto), anim: w.anim };
404
+ return { texto: fraseDeEspera(ahora - w.esperaDesde, w.trasHerramientas), anim: 'ruta' };
410
405
  }
411
406
  export function filaDeTrabajo(w, ahora, ancho = Infinity) {
412
407
  const { texto, anim } = textoDeTrabajo(w, ahora);
@@ -712,6 +707,9 @@ export function opcionesDePregunta(tipo) {
712
707
  return { opciones: [tt('tui.si'), tt('tui.siempre'), tt('tui.no')], cortas: [tt('tui.si'), tt('tui.siempre_corto'), tt('tui.no')], valores: ['si', 'siempre', 'no'], elegida: 0 };
713
708
  }
714
709
  }
710
+ export function esNueva(v) {
711
+ return ALIAS_COMANDO[/^\/(\S+)$/.exec(v.trim())?.[1]?.toLowerCase() ?? ''] === 'new';
712
+ }
715
713
  const MAX_LINEAS = 4000;
716
714
  const MAX_BLOQUES = 1500;
717
715
  const PEGADO_LARGO = 280;
@@ -1045,6 +1043,98 @@ export class Superficie {
1045
1043
  this.escribir(s);
1046
1044
  this.pintar();
1047
1045
  }
1046
+ refrescarUltimo() {
1047
+ const b = this.ultimoBloque();
1048
+ if (!b)
1049
+ return;
1050
+ const k = this.ultimoAlto;
1051
+ if (this.lineas.length < k)
1052
+ return;
1053
+ const nuevas = sinBlancoDoble(b(this.ancho), this.ultimoTrasBlanco);
1054
+ const base = this.lineas.length - k;
1055
+ const viejas = this.lineas.slice(base);
1056
+ if (nuevas.length === k && nuevas.every((l, i) => l === viejas[i]))
1057
+ return;
1058
+ if (nuevas.length < k) {
1059
+ this.bloques[this.bloques.length - 1] = b;
1060
+ this.recomponer();
1061
+ return;
1062
+ }
1063
+ this.lineas.splice(base, k, ...nuevas);
1064
+ this.ultimoAlto = nuevas.length;
1065
+ if (!this.activa) {
1066
+ this.volcadas = this.lineas.length;
1067
+ this.recortarMemoria();
1068
+ return;
1069
+ }
1070
+ const subido = this.subidas > 0 || this.volcadas < base + k;
1071
+ const n = this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1072
+ if (subido) {
1073
+ const desde = this.desplazadas(n) - this.subidas;
1074
+ const cambiaVisible = viejas.some((l, i) => base + i < this.volcadas && base + i >= desde && nuevas[i] !== l);
1075
+ if (cambiaVisible)
1076
+ this.vistaSucia = true;
1077
+ this.recortarMemoria();
1078
+ if (cambiaVisible || nuevas.length !== k)
1079
+ this.pintar();
1080
+ return;
1081
+ }
1082
+ const top = this.filaDelCarril(n);
1083
+ const tapadas = top - this.encimaPintadas;
1084
+ let s = '';
1085
+ for (let i = 0; i < k; i++) {
1086
+ if (nuevas[i] === viejas[i])
1087
+ continue;
1088
+ const fila = base + i - this.desplazadas(n) + 1;
1089
+ if (fila >= 1 && fila < tapadas)
1090
+ s += `\x1b[${fila};1H\x1b[2K${nuevas[i]}`;
1091
+ }
1092
+ if (s)
1093
+ this.escribir(`\x1b[?2026h\x1b7${s}\x1b8\x1b[?2026l`);
1094
+ if (nuevas.length > k) {
1095
+ this.escribir(this.destapar());
1096
+ this.volcar();
1097
+ this.pintar();
1098
+ }
1099
+ }
1100
+ reiniciar(bienvenida, consejos = []) {
1101
+ this.bloques = [];
1102
+ this.lineas = [];
1103
+ this.volcadas = 0;
1104
+ this.subidas = 0;
1105
+ this.vistaSucia = false;
1106
+ this.cola = [];
1107
+ this.aviso = null;
1108
+ this.ultimoAlto = 0;
1109
+ this.ultimoTrasBlanco = false;
1110
+ this.encimaPintadas = 0;
1111
+ this.sobre = null;
1112
+ this.pulsado = null;
1113
+ this.ctrlC = 0;
1114
+ this.alRevelar = null;
1115
+ this.ponerBienvenida(bienvenida, consejos);
1116
+ this.recomponer();
1117
+ }
1118
+ ponerBienvenida(bienvenida, consejos = []) {
1119
+ if (bienvenida)
1120
+ this.bloques.push(bienvenida);
1121
+ this.consejos = [];
1122
+ this.consejosVisibles = false;
1123
+ if (consejos.length) {
1124
+ const ocupado = (bienvenida ? bienvenida(this.ancho).length : 0) + componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1125
+ let n = consejos.length;
1126
+ while (n > 0 && ocupado + bloqueDeConsejos(() => consejos.slice(0, n))(this.ancho).length > this.alto)
1127
+ n--;
1128
+ this.consejos = consejos.slice(0, n);
1129
+ this.consejosVisibles = n > 0;
1130
+ if (n > 0)
1131
+ this.bloques.push(bloqueDeConsejos(() => (this.consejosVisibles ? this.consejos : [])));
1132
+ }
1133
+ }
1134
+ alRevelar = null;
1135
+ revelando(fn) {
1136
+ this.alRevelar = fn;
1137
+ }
1048
1138
  recomponer(alEmpezar = false) {
1049
1139
  if (!this.activa)
1050
1140
  return;
@@ -1098,6 +1188,10 @@ export class Superficie {
1098
1188
  this.volcar();
1099
1189
  this.pintar();
1100
1190
  }
1191
+ nuevaTrasParar() {
1192
+ this.cola = ['/new'];
1193
+ this.mostrarAviso(null);
1194
+ }
1101
1195
  get _vista() {
1102
1196
  return { subidas: this.subidas, pendientes: this.lineas.length - this.volcadas };
1103
1197
  }
@@ -1175,18 +1269,7 @@ export class Superficie {
1175
1269
  if (this.activa)
1176
1270
  return;
1177
1271
  this.activa = true;
1178
- if (bienvenida)
1179
- this.bloques.push(bienvenida);
1180
- if (consejos.length) {
1181
- const ocupado = (bienvenida ? bienvenida(this.ancho).length : 0) + componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1182
- let n = consejos.length;
1183
- while (n > 0 && ocupado + bloqueDeConsejos(() => consejos.slice(0, n))(this.ancho).length > this.alto)
1184
- n--;
1185
- this.consejos = consejos.slice(0, n);
1186
- this.consejosVisibles = n > 0;
1187
- if (n > 0)
1188
- this.bloques.push(bloqueDeConsejos(() => (this.consejosVisibles ? this.consejos : [])));
1189
- }
1272
+ this.ponerBienvenida(bienvenida, consejos);
1190
1273
  this.escribir(this.modosSi());
1191
1274
  try {
1192
1275
  this.o.entrada.setRawMode?.(true);
@@ -1321,7 +1404,7 @@ export class Superficie {
1321
1404
  const r = enCola;
1322
1405
  this.ejemplo = null;
1323
1406
  this.mostrarAviso(this.cola.length ? tt('tui.en_cola_n', { n: this.cola.length }) : null);
1324
- if (r.trim() && r.trim() !== '!')
1407
+ if (r.trim() && r.trim() !== '!' && !esNueva(r.trim()))
1325
1408
  this.anadir(this.bloqueDeEnvio(r));
1326
1409
  return Promise.resolve(this.expandir(r));
1327
1410
  }
@@ -1479,6 +1562,10 @@ export class Superficie {
1479
1562
  this.posHistorial = this.historial.length;
1480
1563
  this.irAlFondo();
1481
1564
  if (this.trabajo || !this.resolverLectura) {
1565
+ if (this.trabajo && this.o.alNuevaEnMarcha && esNueva(v)) {
1566
+ this.o.alNuevaEnMarcha();
1567
+ return;
1568
+ }
1482
1569
  if (v) {
1483
1570
  this.cola.push(crudo);
1484
1571
  this.mostrarAviso(this.cola.length > 1 ? tt('tui.en_cola_n', { n: this.cola.length }) : tt('tui.en_cola'));
@@ -1487,7 +1574,7 @@ export class Superficie {
1487
1574
  }
1488
1575
  const r = this.resolverLectura;
1489
1576
  this.resolverLectura = null;
1490
- if (v && v !== '!')
1577
+ if (v && v !== '!' && !esNueva(v))
1491
1578
  this.anadir(this.bloqueDeEnvio(crudo));
1492
1579
  else
1493
1580
  this.pintar();
@@ -1507,6 +1594,11 @@ export class Superficie {
1507
1594
  }
1508
1595
  if (k.t === 'raton')
1509
1596
  return this.alRaton(k);
1597
+ if (this.alRevelar && k.t !== 'pagina_arriba' && k.t !== 'pagina_abajo' && k.t !== 'fondo') {
1598
+ const f = this.alRevelar;
1599
+ this.alRevelar = null;
1600
+ f();
1601
+ }
1510
1602
  if (k.t === 'pagina_arriba' || k.t === 'pagina_abajo')
1511
1603
  return this.desplazar((k.t === 'pagina_arriba' ? 1 : -1) * Math.max(1, this.filasDeVista() - 2));
1512
1604
  if (k.t === 'fondo')
package/dist/ui.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { animacion, c, capacidades, SIMBOLOS } from "./marca.js";
2
2
  import { cuantos, t } from "./textos.js";
3
3
  import { trozos } from "./diff.js";
4
+ import { caracteresDe, caracteresVisibles, cursorEncendido, duracionDeRevelado, MS_MAX_INTERMEDIO, MS_MAX_RESPUESTA, MS_POR_FOTOGRAMA, revelarLineas, simboloDeCursor } from "./actividad.js";
4
5
  import { anchoDe, envolver, recortar, recortarMedio, rellenar, sinColor } from "./texto.js";
5
6
  import { anchoUtil, bloqueDeCierre, bloqueDeLinea, bloqueDeTexto, filaDeTrabajo, MARGEN, nuevoTrabajo, opcionesDePregunta, ponerTextoDeTrabajo, } from "./tui.js";
6
7
  export function leerRespuesta(texto, tipo = 'normal') {
@@ -89,6 +90,8 @@ export class Pantalla {
89
90
  };
90
91
  }
91
92
  bloque(b, grupo = 'otro') {
93
+ if (this.revelado)
94
+ this.terminarRevelado();
92
95
  const bc = this.conCarril(this.conAire(b, grupo));
93
96
  if (this.superficie) {
94
97
  this.superficie.anadir(bc);
@@ -290,42 +293,147 @@ export class Pantalla {
290
293
  }
291
294
  }
292
295
  texto(md) {
293
- const lineas = md.replace(/\r/g, '').split('\n');
294
- this.bloque((ancho) => {
295
- const util = anchoUtil(ancho);
296
- const pre = ' '.repeat(MARGEN);
297
- const out = [];
298
- let enBloque = false;
299
- for (const linea of lineas) {
300
- if (/^```/.test(linea.trim())) {
301
- enBloque = !enBloque;
302
- continue;
303
- }
304
- if (enBloque) {
305
- out.push(`${pre} ${c.marca(recortar(linea.replace(/\t/g, ' '), util - 2))}`);
306
- continue;
307
- }
308
- const titulo = /^#{1,6}\s+(.*)$/.exec(linea);
309
- if (titulo) {
310
- for (const l of envolver(c.negrita(titulo[1]), util))
311
- out.push(`${pre}${l}`);
312
- continue;
313
- }
314
- let l = linea;
315
- let sangria = '';
316
- const lista = /^(\s*)([-*]|\d+[.)])\s+/.exec(l);
317
- if (lista) {
318
- const marca = /\d/.test(lista[2]) ? lista[2] : SIMBOLOS.punto === '●' ? '·' : '-';
319
- l = `${lista[1]}${c.claro(marca)} ${l.slice(lista[0].length)}`;
320
- sangria = ' '.repeat(anchoDe(lista[1]) + anchoDe(marca) + 1);
321
- }
322
- l = l.replace(/\*\*([^*]+)\*\*/g, (_m, x) => c.negrita(x));
323
- l = l.replace(/`([^`]+)`/g, (_m, x) => c.marca(x));
324
- for (const p of envolver(l, util, sangria))
325
- out.push(`${pre}${p}`);
296
+ this.terminarRevelado();
297
+ this.bloque(bloqueDeMarkdown(md), 'texto');
298
+ }
299
+ animar = true;
300
+ revelado = null;
301
+ terminarRevelado() {
302
+ const f = this.revelado;
303
+ this.revelado = null;
304
+ f?.();
305
+ }
306
+ textoAnimado(md, o = {}) {
307
+ this.terminarRevelado();
308
+ const conSuperficie = this.superficie !== null;
309
+ if (!this.animar || this.carril || !this.tty || (!conSuperficie && !process.stdout.isTTY)) {
310
+ this.texto(md);
311
+ return Promise.resolve();
312
+ }
313
+ const completo = bloqueDeMarkdown(md);
314
+ const maxMs = o.intermedio ? MS_MAX_INTERMEDIO : MS_MAX_RESPUESTA;
315
+ const estado = { inicio: Date.now(), fin: false, ahora: Date.now() };
316
+ const total = caracteresDe(completo(this.superficie?.ancho ?? Math.max(20, process.stdout.columns || 80)));
317
+ const duracion = duracionDeRevelado(total, maxMs);
318
+ const parcial = (ancho) => {
319
+ const lineas = completo(ancho);
320
+ if (estado.fin)
321
+ return lineas;
322
+ const ms = estado.ahora - estado.inicio;
323
+ const n = caracteresVisibles(caracteresDe(lineas), ms, duracion);
324
+ return revelarLineas(lineas, n, cursorEncendido(ms) ? simboloDeCursor() : ' ', anchoUtil(ancho) + MARGEN * 2);
325
+ };
326
+ return new Promise((resolver) => {
327
+ let reloj = null;
328
+ const acabar = () => {
329
+ if (estado.fin)
330
+ return;
331
+ estado.fin = true;
332
+ if (reloj)
333
+ clearInterval(reloj);
334
+ if (this.revelado === acabar)
335
+ this.revelado = null;
336
+ this.superficie?.revelando(null);
337
+ if (conSuperficie)
338
+ this.superficie.refrescarUltimo();
339
+ else
340
+ this.revelarEnLinea(completo, Infinity);
341
+ resolver();
342
+ };
343
+ if (conSuperficie) {
344
+ this.bloque(parcial, 'texto');
345
+ this.superficie.revelando(acabar);
326
346
  }
327
- return out;
328
- }, 'texto');
347
+ else {
348
+ this.pausarLinea();
349
+ this.ultimoGrupo = 'texto';
350
+ this.lineaEnCurso = { escritas: 0, parcial: '' };
351
+ this.revelarEnLinea(completo, 0);
352
+ }
353
+ this.revelado = acabar;
354
+ const fotograma = () => {
355
+ estado.ahora = Date.now();
356
+ if (estado.ahora - estado.inicio >= duracion)
357
+ return acabar();
358
+ if (conSuperficie)
359
+ this.superficie.refrescarUltimo();
360
+ else
361
+ this.revelarEnLinea(completo, caracteresVisibles(total, estado.ahora - estado.inicio, duracion), cursorEncendido(estado.ahora - estado.inicio));
362
+ };
363
+ reloj = setInterval(fotograma, MS_POR_FOTOGRAMA);
364
+ reloj.unref?.();
365
+ });
329
366
  }
367
+ lineaEnCurso = null;
368
+ revelarEnLinea(completo, n, cursor = false) {
369
+ const l = this.lineaEnCurso;
370
+ if (!l)
371
+ return;
372
+ const ancho = Math.max(20, process.stdout.columns || capacidades().ancho || 80);
373
+ const lineas = completo(ancho);
374
+ const vistas = revelarLineas(lineas, n, null);
375
+ let s = '';
376
+ while (l.escritas < vistas.length - 1 || (n >= caracteresDe(lineas) && l.escritas < lineas.length)) {
377
+ s += `\r\x1b[2K${lineas[l.escritas]}\n`;
378
+ l.escritas++;
379
+ l.parcial = '';
380
+ }
381
+ if (n < caracteresDe(lineas)) {
382
+ const parcial = vistas[l.escritas] ?? '';
383
+ s += `\r\x1b[2K${parcial}${cursor ? `${anchoDe(parcial) ? ' ' : ''}${simboloDeCursor()}` : ''}`;
384
+ l.parcial = parcial;
385
+ }
386
+ else {
387
+ this.lineaEnCurso = null;
388
+ if (lineas.length)
389
+ this.enBlanco = lineas[lineas.length - 1].trim() === '';
390
+ }
391
+ if (s)
392
+ process.stdout.write(s);
393
+ }
394
+ reiniciar() {
395
+ this.terminarRevelado();
396
+ this.ultimoGrupo = null;
397
+ this.enBlanco = true;
398
+ this.lecturas = null;
399
+ }
400
+ }
401
+ export function bloqueDeMarkdown(md) {
402
+ const lineas = md.replace(/\r/g, '').split('\n');
403
+ return (ancho) => {
404
+ const util = anchoUtil(ancho);
405
+ const pre = ' '.repeat(MARGEN);
406
+ const out = [];
407
+ let enBloque = false;
408
+ for (const linea of lineas) {
409
+ if (/^```/.test(linea.trim())) {
410
+ enBloque = !enBloque;
411
+ continue;
412
+ }
413
+ if (enBloque) {
414
+ out.push(`${pre} ${c.marca(recortar(linea.replace(/\t/g, ' '), util - 2))}`);
415
+ continue;
416
+ }
417
+ const titulo = /^#{1,6}\s+(.*)$/.exec(linea);
418
+ if (titulo) {
419
+ for (const l of envolver(c.negrita(titulo[1]), util))
420
+ out.push(`${pre}${l}`);
421
+ continue;
422
+ }
423
+ let l = linea;
424
+ let sangria = '';
425
+ const lista = /^(\s*)([-*]|\d+[.)])\s+/.exec(l);
426
+ if (lista) {
427
+ const marca = /\d/.test(lista[2]) ? lista[2] : SIMBOLOS.punto === '●' ? '·' : '-';
428
+ l = `${lista[1]}${c.claro(marca)} ${l.slice(lista[0].length)}`;
429
+ sangria = ' '.repeat(anchoDe(lista[1]) + anchoDe(marca) + 1);
430
+ }
431
+ l = l.replace(/\*\*([^*]+)\*\*/g, (_m, x) => c.negrita(x));
432
+ l = l.replace(/`([^`]+)`/g, (_m, x) => c.marca(x));
433
+ for (const p of envolver(l, util, sangria))
434
+ out.push(`${pre}${p}`);
435
+ }
436
+ return out;
437
+ };
330
438
  }
331
439
  export { bloqueDeLinea };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getparallel/parx",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "parx: the coding agent of Parallel in your terminal. Reads and edits your project, runs commands and tests, and does coding tasks end to end. Mac, Windows and Linux.",
5
5
  "keywords": [
6
6
  "parx",